From 122ddd3e72552e9efef12a66043981459837bede Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Nov 2024 08:11:40 -0400 Subject: [PATCH 01/56] chore(deps-dev): bump coverage from 7.6.7 to 7.6.8 (#5895) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- poetry.lock | 128 ++++++++++++++++++++++++------------------------- pyproject.toml | 2 +- 2 files changed, 65 insertions(+), 65 deletions(-) diff --git a/poetry.lock b/poetry.lock index 456759fa56..a3953331d3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1099,73 +1099,73 @@ files = [ [[package]] name = "coverage" -version = "7.6.7" +version = "7.6.8" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.9" files = [ - {file = "coverage-7.6.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:108bb458827765d538abcbf8288599fee07d2743357bdd9b9dad456c287e121e"}, - {file = "coverage-7.6.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c973b2fe4dc445cb865ab369df7521df9c27bf40715c837a113edaa2aa9faf45"}, - {file = "coverage-7.6.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c6b24007c4bcd0b19fac25763a7cac5035c735ae017e9a349b927cfc88f31c1"}, - {file = "coverage-7.6.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:acbb8af78f8f91b3b51f58f288c0994ba63c646bc1a8a22ad072e4e7e0a49f1c"}, - {file = "coverage-7.6.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad32a981bcdedb8d2ace03b05e4fd8dace8901eec64a532b00b15217d3677dd2"}, - {file = "coverage-7.6.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:34d23e28ccb26236718a3a78ba72744212aa383141961dd6825f6595005c8b06"}, - {file = "coverage-7.6.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e25bacb53a8c7325e34d45dddd2f2fbae0dbc230d0e2642e264a64e17322a777"}, - {file = "coverage-7.6.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af05bbba896c4472a29408455fe31b3797b4d8648ed0a2ccac03e074a77e2314"}, - {file = "coverage-7.6.7-cp310-cp310-win32.whl", hash = "sha256:796c9b107d11d2d69e1849b2dfe41730134b526a49d3acb98ca02f4985eeff7a"}, - {file = "coverage-7.6.7-cp310-cp310-win_amd64.whl", hash = "sha256:987a8e3da7da4eed10a20491cf790589a8e5e07656b6dc22d3814c4d88faf163"}, - {file = "coverage-7.6.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7e61b0e77ff4dddebb35a0e8bb5a68bf0f8b872407d8d9f0c726b65dfabe2469"}, - {file = "coverage-7.6.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1a5407a75ca4abc20d6252efeb238377a71ce7bda849c26c7a9bece8680a5d99"}, - {file = "coverage-7.6.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df002e59f2d29e889c37abd0b9ee0d0e6e38c24f5f55d71ff0e09e3412a340ec"}, - {file = "coverage-7.6.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:673184b3156cba06154825f25af33baa2671ddae6343f23175764e65a8c4c30b"}, - {file = "coverage-7.6.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e69ad502f1a2243f739f5bd60565d14a278be58be4c137d90799f2c263e7049a"}, - {file = "coverage-7.6.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:60dcf7605c50ea72a14490d0756daffef77a5be15ed1b9fea468b1c7bda1bc3b"}, - {file = "coverage-7.6.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9c2eb378bebb2c8f65befcb5147877fc1c9fbc640fc0aad3add759b5df79d55d"}, - {file = "coverage-7.6.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3c0317288f032221d35fa4cbc35d9f4923ff0dfd176c79c9b356e8ef8ef2dff4"}, - {file = "coverage-7.6.7-cp311-cp311-win32.whl", hash = "sha256:951aade8297358f3618a6e0660dc74f6b52233c42089d28525749fc8267dccd2"}, - {file = "coverage-7.6.7-cp311-cp311-win_amd64.whl", hash = "sha256:5e444b8e88339a2a67ce07d41faabb1d60d1004820cee5a2c2b54e2d8e429a0f"}, - {file = "coverage-7.6.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f07ff574986bc3edb80e2c36391678a271d555f91fd1d332a1e0f4b5ea4b6ea9"}, - {file = "coverage-7.6.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:49ed5ee4109258973630c1f9d099c7e72c5c36605029f3a91fe9982c6076c82b"}, - {file = "coverage-7.6.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3e8796434a8106b3ac025fd15417315d7a58ee3e600ad4dbcfddc3f4b14342c"}, - {file = "coverage-7.6.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3b925300484a3294d1c70f6b2b810d6526f2929de954e5b6be2bf8caa1f12c1"}, - {file = "coverage-7.6.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c42ec2c522e3ddd683dec5cdce8e62817afb648caedad9da725001fa530d354"}, - {file = "coverage-7.6.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0266b62cbea568bd5e93a4da364d05de422110cbed5056d69339bd5af5685433"}, - {file = "coverage-7.6.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e5f2a0f161d126ccc7038f1f3029184dbdf8f018230af17ef6fd6a707a5b881f"}, - {file = "coverage-7.6.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c132b5a22821f9b143f87446805e13580b67c670a548b96da945a8f6b4f2efbb"}, - {file = "coverage-7.6.7-cp312-cp312-win32.whl", hash = "sha256:7c07de0d2a110f02af30883cd7dddbe704887617d5c27cf373362667445a4c76"}, - {file = "coverage-7.6.7-cp312-cp312-win_amd64.whl", hash = "sha256:fd49c01e5057a451c30c9b892948976f5d38f2cbd04dc556a82743ba8e27ed8c"}, - {file = "coverage-7.6.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:46f21663e358beae6b368429ffadf14ed0a329996248a847a4322fb2e35d64d3"}, - {file = "coverage-7.6.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:40cca284c7c310d622a1677f105e8507441d1bb7c226f41978ba7c86979609ab"}, - {file = "coverage-7.6.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77256ad2345c29fe59ae861aa11cfc74579c88d4e8dbf121cbe46b8e32aec808"}, - {file = "coverage-7.6.7-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87ea64b9fa52bf395272e54020537990a28078478167ade6c61da7ac04dc14bc"}, - {file = "coverage-7.6.7-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d608a7808793e3615e54e9267519351c3ae204a6d85764d8337bd95993581a8"}, - {file = "coverage-7.6.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdd94501d65adc5c24f8a1a0eda110452ba62b3f4aeaba01e021c1ed9cb8f34a"}, - {file = "coverage-7.6.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:82c809a62e953867cf57e0548c2b8464207f5f3a6ff0e1e961683e79b89f2c55"}, - {file = "coverage-7.6.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bb684694e99d0b791a43e9fc0fa58efc15ec357ac48d25b619f207c41f2fd384"}, - {file = "coverage-7.6.7-cp313-cp313-win32.whl", hash = "sha256:963e4a08cbb0af6623e61492c0ec4c0ec5c5cf74db5f6564f98248d27ee57d30"}, - {file = "coverage-7.6.7-cp313-cp313-win_amd64.whl", hash = "sha256:14045b8bfd5909196a90da145a37f9d335a5d988a83db34e80f41e965fb7cb42"}, - {file = "coverage-7.6.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f2c7a045eef561e9544359a0bf5784b44e55cefc7261a20e730baa9220c83413"}, - {file = "coverage-7.6.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dd4e4a49d9c72a38d18d641135d2fb0bdf7b726ca60a103836b3d00a1182acd"}, - {file = "coverage-7.6.7-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c95e0fa3d1547cb6f021ab72f5c23402da2358beec0a8e6d19a368bd7b0fb37"}, - {file = "coverage-7.6.7-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f63e21ed474edd23f7501f89b53280014436e383a14b9bd77a648366c81dce7b"}, - {file = "coverage-7.6.7-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ead9b9605c54d15be228687552916c89c9683c215370c4a44f1f217d2adcc34d"}, - {file = "coverage-7.6.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:0573f5cbf39114270842d01872952d301027d2d6e2d84013f30966313cadb529"}, - {file = "coverage-7.6.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:e2c8e3384c12dfa19fa9a52f23eb091a8fad93b5b81a41b14c17c78e23dd1d8b"}, - {file = "coverage-7.6.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:70a56a2ec1869e6e9fa69ef6b76b1a8a7ef709972b9cc473f9ce9d26b5997ce3"}, - {file = "coverage-7.6.7-cp313-cp313t-win32.whl", hash = "sha256:dbba8210f5067398b2c4d96b4e64d8fb943644d5eb70be0d989067c8ca40c0f8"}, - {file = "coverage-7.6.7-cp313-cp313t-win_amd64.whl", hash = "sha256:dfd14bcae0c94004baba5184d1c935ae0d1231b8409eb6c103a5fd75e8ecdc56"}, - {file = "coverage-7.6.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:37a15573f988b67f7348916077c6d8ad43adb75e478d0910957394df397d2874"}, - {file = "coverage-7.6.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b6cce5c76985f81da3769c52203ee94722cd5d5889731cd70d31fee939b74bf0"}, - {file = "coverage-7.6.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ab9763d291a17b527ac6fd11d1a9a9c358280adb320e9c2672a97af346ac2c"}, - {file = "coverage-7.6.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6cf96ceaa275f071f1bea3067f8fd43bec184a25a962c754024c973af871e1b7"}, - {file = "coverage-7.6.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aee9cf6b0134d6f932d219ce253ef0e624f4fa588ee64830fcba193269e4daa3"}, - {file = "coverage-7.6.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2bc3e45c16564cc72de09e37413262b9f99167803e5e48c6156bccdfb22c8327"}, - {file = "coverage-7.6.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:623e6965dcf4e28a3debaa6fcf4b99ee06d27218f46d43befe4db1c70841551c"}, - {file = "coverage-7.6.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:850cfd2d6fc26f8346f422920ac204e1d28814e32e3a58c19c91980fa74d8289"}, - {file = "coverage-7.6.7-cp39-cp39-win32.whl", hash = "sha256:c296263093f099da4f51b3dff1eff5d4959b527d4f2f419e16508c5da9e15e8c"}, - {file = "coverage-7.6.7-cp39-cp39-win_amd64.whl", hash = "sha256:90746521206c88bdb305a4bf3342b1b7316ab80f804d40c536fc7d329301ee13"}, - {file = "coverage-7.6.7-pp39.pp310-none-any.whl", hash = "sha256:0ddcb70b3a3a57581b450571b31cb774f23eb9519c2aaa6176d3a84c9fc57671"}, - {file = "coverage-7.6.7.tar.gz", hash = "sha256:d79d4826e41441c9a118ff045e4bccb9fdbdcb1d02413e7ea6eb5c87b5439d24"}, + {file = "coverage-7.6.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b39e6011cd06822eb964d038d5dff5da5d98652b81f5ecd439277b32361a3a50"}, + {file = "coverage-7.6.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:63c19702db10ad79151a059d2d6336fe0c470f2e18d0d4d1a57f7f9713875dcf"}, + {file = "coverage-7.6.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3985b9be361d8fb6b2d1adc9924d01dec575a1d7453a14cccd73225cb79243ee"}, + {file = "coverage-7.6.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:644ec81edec0f4ad17d51c838a7d01e42811054543b76d4ba2c5d6af741ce2a6"}, + {file = "coverage-7.6.8-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f188a2402f8359cf0c4b1fe89eea40dc13b52e7b4fd4812450da9fcd210181d"}, + {file = "coverage-7.6.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e19122296822deafce89a0c5e8685704c067ae65d45e79718c92df7b3ec3d331"}, + {file = "coverage-7.6.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:13618bed0c38acc418896005732e565b317aa9e98d855a0e9f211a7ffc2d6638"}, + {file = "coverage-7.6.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:193e3bffca48ad74b8c764fb4492dd875038a2f9925530cb094db92bb5e47bed"}, + {file = "coverage-7.6.8-cp310-cp310-win32.whl", hash = "sha256:3988665ee376abce49613701336544041f2117de7b7fbfe91b93d8ff8b151c8e"}, + {file = "coverage-7.6.8-cp310-cp310-win_amd64.whl", hash = "sha256:f56f49b2553d7dd85fd86e029515a221e5c1f8cb3d9c38b470bc38bde7b8445a"}, + {file = "coverage-7.6.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:86cffe9c6dfcfe22e28027069725c7f57f4b868a3f86e81d1c62462764dc46d4"}, + {file = "coverage-7.6.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d82ab6816c3277dc962cfcdc85b1efa0e5f50fb2c449432deaf2398a2928ab94"}, + {file = "coverage-7.6.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13690e923a3932e4fad4c0ebfb9cb5988e03d9dcb4c5150b5fcbf58fd8bddfc4"}, + {file = "coverage-7.6.8-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4be32da0c3827ac9132bb488d331cb32e8d9638dd41a0557c5569d57cf22c9c1"}, + {file = "coverage-7.6.8-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44e6c85bbdc809383b509d732b06419fb4544dca29ebe18480379633623baafb"}, + {file = "coverage-7.6.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:768939f7c4353c0fac2f7c37897e10b1414b571fd85dd9fc49e6a87e37a2e0d8"}, + {file = "coverage-7.6.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e44961e36cb13c495806d4cac67640ac2866cb99044e210895b506c26ee63d3a"}, + {file = "coverage-7.6.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ea8bb1ab9558374c0ab591783808511d135a833c3ca64a18ec927f20c4030f0"}, + {file = "coverage-7.6.8-cp311-cp311-win32.whl", hash = "sha256:629a1ba2115dce8bf75a5cce9f2486ae483cb89c0145795603d6554bdc83e801"}, + {file = "coverage-7.6.8-cp311-cp311-win_amd64.whl", hash = "sha256:fb9fc32399dca861584d96eccd6c980b69bbcd7c228d06fb74fe53e007aa8ef9"}, + {file = "coverage-7.6.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e683e6ecc587643f8cde8f5da6768e9d165cd31edf39ee90ed7034f9ca0eefee"}, + {file = "coverage-7.6.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1defe91d41ce1bd44b40fabf071e6a01a5aa14de4a31b986aa9dfd1b3e3e414a"}, + {file = "coverage-7.6.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7ad66e8e50225ebf4236368cc43c37f59d5e6728f15f6e258c8639fa0dd8e6d"}, + {file = "coverage-7.6.8-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fe47da3e4fda5f1abb5709c156eca207eacf8007304ce3019eb001e7a7204cb"}, + {file = "coverage-7.6.8-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:202a2d645c5a46b84992f55b0a3affe4f0ba6b4c611abec32ee88358db4bb649"}, + {file = "coverage-7.6.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4674f0daa1823c295845b6a740d98a840d7a1c11df00d1fd62614545c1583787"}, + {file = "coverage-7.6.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:74610105ebd6f33d7c10f8907afed696e79c59e3043c5f20eaa3a46fddf33b4c"}, + {file = "coverage-7.6.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37cda8712145917105e07aab96388ae76e787270ec04bcb9d5cc786d7cbb8443"}, + {file = "coverage-7.6.8-cp312-cp312-win32.whl", hash = "sha256:9e89d5c8509fbd6c03d0dd1972925b22f50db0792ce06324ba069f10787429ad"}, + {file = "coverage-7.6.8-cp312-cp312-win_amd64.whl", hash = "sha256:379c111d3558272a2cae3d8e57e6b6e6f4fe652905692d54bad5ea0ca37c5ad4"}, + {file = "coverage-7.6.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0b0c69f4f724c64dfbfe79f5dfb503b42fe6127b8d479b2677f2b227478db2eb"}, + {file = "coverage-7.6.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c15b32a7aca8038ed7644f854bf17b663bc38e1671b5d6f43f9a2b2bd0c46f63"}, + {file = "coverage-7.6.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63068a11171e4276f6ece913bde059e77c713b48c3a848814a6537f35afb8365"}, + {file = "coverage-7.6.8-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f4548c5ead23ad13fb7a2c8ea541357474ec13c2b736feb02e19a3085fac002"}, + {file = "coverage-7.6.8-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b4b4299dd0d2c67caaaf286d58aef5e75b125b95615dda4542561a5a566a1e3"}, + {file = "coverage-7.6.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9ebfb2507751f7196995142f057d1324afdab56db1d9743aab7f50289abd022"}, + {file = "coverage-7.6.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c1b4474beee02ede1eef86c25ad4600a424fe36cff01a6103cb4533c6bf0169e"}, + {file = "coverage-7.6.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d9fd2547e6decdbf985d579cf3fc78e4c1d662b9b0ff7cc7862baaab71c9cc5b"}, + {file = "coverage-7.6.8-cp313-cp313-win32.whl", hash = "sha256:8aae5aea53cbfe024919715eca696b1a3201886ce83790537d1c3668459c7146"}, + {file = "coverage-7.6.8-cp313-cp313-win_amd64.whl", hash = "sha256:ae270e79f7e169ccfe23284ff5ea2d52a6f401dc01b337efb54b3783e2ce3f28"}, + {file = "coverage-7.6.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:de38add67a0af869b0d79c525d3e4588ac1ffa92f39116dbe0ed9753f26eba7d"}, + {file = "coverage-7.6.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b07c25d52b1c16ce5de088046cd2432b30f9ad5e224ff17c8f496d9cb7d1d451"}, + {file = "coverage-7.6.8-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62a66ff235e4c2e37ed3b6104d8b478d767ff73838d1222132a7a026aa548764"}, + {file = "coverage-7.6.8-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09b9f848b28081e7b975a3626e9081574a7b9196cde26604540582da60235fdf"}, + {file = "coverage-7.6.8-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:093896e530c38c8e9c996901858ac63f3d4171268db2c9c8b373a228f459bbc5"}, + {file = "coverage-7.6.8-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a7b8ac36fd688c8361cbc7bf1cb5866977ece6e0b17c34aa0df58bda4fa18a4"}, + {file = "coverage-7.6.8-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:38c51297b35b3ed91670e1e4efb702b790002e3245a28c76e627478aa3c10d83"}, + {file = "coverage-7.6.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2e4e0f60cb4bd7396108823548e82fdab72d4d8a65e58e2c19bbbc2f1e2bfa4b"}, + {file = "coverage-7.6.8-cp313-cp313t-win32.whl", hash = "sha256:6535d996f6537ecb298b4e287a855f37deaf64ff007162ec0afb9ab8ba3b8b71"}, + {file = "coverage-7.6.8-cp313-cp313t-win_amd64.whl", hash = "sha256:c79c0685f142ca53256722a384540832420dff4ab15fec1863d7e5bc8691bdcc"}, + {file = "coverage-7.6.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3ac47fa29d8d41059ea3df65bd3ade92f97ee4910ed638e87075b8e8ce69599e"}, + {file = "coverage-7.6.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:24eda3a24a38157eee639ca9afe45eefa8d2420d49468819ac5f88b10de84f4c"}, + {file = "coverage-7.6.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4c81ed2820b9023a9a90717020315e63b17b18c274a332e3b6437d7ff70abe0"}, + {file = "coverage-7.6.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd55f8fc8fa494958772a2a7302b0354ab16e0b9272b3c3d83cdb5bec5bd1779"}, + {file = "coverage-7.6.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f39e2f3530ed1626c66e7493be7a8423b023ca852aacdc91fb30162c350d2a92"}, + {file = "coverage-7.6.8-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:716a78a342679cd1177bc8c2fe957e0ab91405bd43a17094324845200b2fddf4"}, + {file = "coverage-7.6.8-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:177f01eeaa3aee4a5ffb0d1439c5952b53d5010f86e9d2667963e632e30082cc"}, + {file = "coverage-7.6.8-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:912e95017ff51dc3d7b6e2be158dedc889d9a5cc3382445589ce554f1a34c0ea"}, + {file = "coverage-7.6.8-cp39-cp39-win32.whl", hash = "sha256:4db3ed6a907b555e57cc2e6f14dc3a4c2458cdad8919e40b5357ab9b6db6c43e"}, + {file = "coverage-7.6.8-cp39-cp39-win_amd64.whl", hash = "sha256:428ac484592f780e8cd7b6b14eb568f7c85460c92e2a37cb0c0e5186e1a0d076"}, + {file = "coverage-7.6.8-pp39.pp310-none-any.whl", hash = "sha256:5c52a036535d12590c32c49209e79cabaad9f9ad8aa4cbd875b68c4d67a9cbce"}, + {file = "coverage-7.6.8.tar.gz", hash = "sha256:8b2b8503edb06822c86d82fa64a4a5cb0760bb8f31f26e138ec743f422f37cfc"}, ] [package.dependencies] @@ -5194,4 +5194,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.13" -content-hash = "b1a5cce3381c4b6a115913bdeb69546cbedecc60803e5b33798187aa82641e77" +content-hash = "69fe125519b74e4c6633b0d620c74f71b4e8a8041a052ca8796c03c6301a25a9" diff --git a/pyproject.toml b/pyproject.toml index f0354fdadc..e1be889836 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,7 +77,7 @@ tzlocal = "5.2" [tool.poetry.group.dev.dependencies] bandit = "1.7.10" black = "24.10.0" -coverage = "7.6.7" +coverage = "7.6.8" docker = "7.1.0" flake8 = "7.1.1" freezegun = "1.5.1" From 3e9b4d34bdc52c8b9262224ed89781a7977be8bb Mon Sep 17 00:00:00 2001 From: Prowler Bot Date: Tue, 26 Nov 2024 13:12:21 +0100 Subject: [PATCH 02/56] chore(regions_update): Changes in regions for AWS services (#5905) Co-authored-by: sergargar <38561120+sergargar@users.noreply.github.com> --- prowler/providers/aws/aws_regions_by_service.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/prowler/providers/aws/aws_regions_by_service.json b/prowler/providers/aws/aws_regions_by_service.json index 7d6567c5e6..fed0f3a69b 100644 --- a/prowler/providers/aws/aws_regions_by_service.json +++ b/prowler/providers/aws/aws_regions_by_service.json @@ -10098,6 +10098,15 @@ ] } }, + "socialmessaging": { + "regions": { + "aws": [ + "eu-central-1" + ], + "aws-cn": [], + "aws-us-gov": [] + } + }, "sqs": { "regions": { "aws": [ From 9c383baff309d868b37934b694df9aacba397fad Mon Sep 17 00:00:00 2001 From: Mario Rodriguez Lopez <101330800+MarioRgzLpz@users.noreply.github.com> Date: Tue, 26 Nov 2024 13:24:45 +0100 Subject: [PATCH 03/56] fix(ec2): Change `ec2_sg_high_risk_ports` configurable parameter name (#5904) --- docs/tutorials/configuration_file.md | 4 ++-- prowler/config/config.yaml | 2 +- tests/config/config_test.py | 2 +- tests/config/fixtures/config.yaml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/tutorials/configuration_file.md b/docs/tutorials/configuration_file.md index d4ef971d41..866c7f8dff 100644 --- a/docs/tutorials/configuration_file.md +++ b/docs/tutorials/configuration_file.md @@ -41,7 +41,7 @@ The following list includes all the AWS checks with configurable variables that | `ec2_launch_template_no_secrets` | `secrets_ignore_patterns` | List of Strings | | `ec2_securitygroup_allow_ingress_from_internet_to_any_port` | `ec2_allowed_instance_owners` | List of Strings | | `ec2_securitygroup_allow_ingress_from_internet_to_any_port` | `ec2_allowed_interface_types` | List of Strings | -| `ec2_securitygroup_allow_ingress_from_internet_to_high_risk_tcp_ports`| `ec2_sg_high_risk_ports` | List of Integer | +| `ec2_securitygroup_allow_ingress_from_internet_to_high_risk_tcp_ports`| `ec2_high_risk_ports` | List of Integer | | `ec2_securitygroup_with_many_ingress_egress_rules` | `max_security_group_rules` | Integer | | `ecs_task_definitions_no_environment_secrets` | `secrets_ignore_patterns` | List of Strings | | `ecr_repositories_scan_vulnerabilities_in_latest_image` | `ecr_repository_vulnerability_minimum_severity` | String | @@ -144,7 +144,7 @@ aws: "amazon-elb" ] # aws.ec2_securitygroup_allow_ingress_from_internet_to_high_risk_tcp_ports - ec2_sg_high_risk_ports: + ec2_high_risk_ports: [ 25, 110, diff --git a/prowler/config/config.yaml b/prowler/config/config.yaml index 7c42c521c0..8b6947b458 100644 --- a/prowler/config/config.yaml +++ b/prowler/config/config.yaml @@ -42,7 +42,7 @@ aws: "amazon-elb" ] # aws.ec2_securitygroup_allow_ingress_from_internet_to_high_risk_tcp_ports - ec2_sg_high_risk_ports: + ec2_high_risk_ports: [ 25, 110, diff --git a/tests/config/config_test.py b/tests/config/config_test.py index 98e1e5f43e..f79ef50eff 100644 --- a/tests/config/config_test.py +++ b/tests/config/config_test.py @@ -79,7 +79,7 @@ config_aws = { "max_ec2_instance_age_in_days": 180, "ec2_allowed_interface_types": ["api_gateway_managed", "vpc_endpoint"], "ec2_allowed_instance_owners": ["amazon-elb"], - "ec2_sg_high_risk_ports": [ + "ec2_high_risk_ports": [ 25, 110, 135, diff --git a/tests/config/fixtures/config.yaml b/tests/config/fixtures/config.yaml index d769b83404..54b1c011b3 100644 --- a/tests/config/fixtures/config.yaml +++ b/tests/config/fixtures/config.yaml @@ -42,7 +42,7 @@ aws: "amazon-elb" ] # aws.ec2_securitygroup_allow_ingress_from_internet_to_high_risk_tcp_ports - ec2_sg_high_risk_ports: + ec2_high_risk_ports: [ 25, 110, From 9a666891fdec8472316b9b4338e7b678f37100c9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Nov 2024 09:45:31 -0400 Subject: [PATCH 04/56] chore(deps-dev): bump mkdocs-material from 9.5.45 to 9.5.46 (#5894) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- poetry.lock | 8 ++++---- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/poetry.lock b/poetry.lock index a3953331d3..d7b8fca61f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2583,13 +2583,13 @@ dev = ["click", "codecov", "mkdocs-gen-files", "mkdocs-git-authors-plugin", "mkd [[package]] name = "mkdocs-material" -version = "9.5.45" +version = "9.5.46" description = "Documentation that simply works" optional = false python-versions = ">=3.8" files = [ - {file = "mkdocs_material-9.5.45-py3-none-any.whl", hash = "sha256:a9be237cfd0be14be75f40f1726d83aa3a81ce44808dc3594d47a7a592f44547"}, - {file = "mkdocs_material-9.5.45.tar.gz", hash = "sha256:286489cf0beca4a129d91d59d6417419c63bceed1ce5cd0ec1fc7e1ebffb8189"}, + {file = "mkdocs_material-9.5.46-py3-none-any.whl", hash = "sha256:98f0a2039c62e551a68aad0791a8d41324ff90c03a6e6cea381a384b84908b83"}, + {file = "mkdocs_material-9.5.46.tar.gz", hash = "sha256:ae2043f4238e572f9a40e0b577f50400d6fc31e2fef8ea141800aebf3bd273d7"}, ] [package.dependencies] @@ -5194,4 +5194,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.13" -content-hash = "69fe125519b74e4c6633b0d620c74f71b4e8a8041a052ca8796c03c6301a25a9" +content-hash = "951867954f393582007c653ec643754099a8b5df909a81bd6f0cd5c8ec3326a9" diff --git a/pyproject.toml b/pyproject.toml index e1be889836..c71a0485cd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -100,7 +100,7 @@ optional = true [tool.poetry.group.docs.dependencies] mkdocs = "1.6.1" mkdocs-git-revision-date-localized-plugin = "1.3.0" -mkdocs-material = "9.5.45" +mkdocs-material = "9.5.46" mkdocs-material-extensions = "1.3.1" [tool.poetry.scripts] From 0b2e1f1917d1e6586a04d6c2e7d7c77aa3937f3a Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Tue, 26 Nov 2024 16:09:56 +0100 Subject: [PATCH 05/56] feat: configure codeql for ui repository (#5912) --- .github/codeql/ui-codeql-config.yml | 3 ++ .github/workflows/ui-codeql.yml | 61 +++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 .github/codeql/ui-codeql-config.yml create mode 100644 .github/workflows/ui-codeql.yml diff --git a/.github/codeql/ui-codeql-config.yml b/.github/codeql/ui-codeql-config.yml new file mode 100644 index 0000000000..62ebee5617 --- /dev/null +++ b/.github/codeql/ui-codeql-config.yml @@ -0,0 +1,3 @@ +name: "Custom CodeQL Config for UI" +paths: + - "ui/" diff --git a/.github/workflows/ui-codeql.yml b/.github/workflows/ui-codeql.yml new file mode 100644 index 0000000000..2765921cf6 --- /dev/null +++ b/.github/workflows/ui-codeql.yml @@ -0,0 +1,61 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "UI - CodeQL" + +on: + push: + branches: + - "master" + - "v4.*" + - "v5.*" + paths: + - "ui/**" + pull_request: + branches: + - "master" + - "v4.*" + - "v5.*" + paths: + - "ui/**" + schedule: + - cron: "00 12 * * *" + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: ["javascript"] + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + config-file: ./.github/codeql/ui-codeql-config.yml + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" From d57db6c39e39ccf9b94476728220bc86f42c2e8e Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Tue, 26 Nov 2024 16:17:26 +0100 Subject: [PATCH 06/56] feat(ui:overview): add new fail findings to date table (#5906) --- ui/app/(prowler)/page.tsx | 107 +++++++-- ui/components/overview/index.ts | 1 + .../link-to-findings/link-to-findings.tsx | 21 ++ .../table/column-new-findings-to-date.tsx | 43 ++++ .../table/finding-detail.tsx | 219 ++++++++++++++++++ .../new-findings-table/table/index.ts | 2 + .../table/skeleton-table-new-findings.tsx | 65 ++++++ .../provider-overview/provider-overview.tsx | 59 ++++- .../users/table/data-table-row-actions.tsx | 6 +- 9 files changed, 496 insertions(+), 27 deletions(-) create mode 100644 ui/components/overview/new-findings-table/link-to-findings/link-to-findings.tsx create mode 100644 ui/components/overview/new-findings-table/table/column-new-findings-to-date.tsx create mode 100644 ui/components/overview/new-findings-table/table/finding-detail.tsx create mode 100644 ui/components/overview/new-findings-table/table/index.ts create mode 100644 ui/components/overview/new-findings-table/table/skeleton-table-new-findings.tsx diff --git a/ui/app/(prowler)/page.tsx b/ui/app/(prowler)/page.tsx index bae0d835c4..df260ec8e3 100644 --- a/ui/app/(prowler)/page.tsx +++ b/ui/app/(prowler)/page.tsx @@ -1,30 +1,53 @@ import { Spacer } from "@nextui-org/react"; import { Suspense } from "react"; +import { getFindings } from "@/actions/findings/findings"; import { getProvidersOverview } from "@/actions/overview/overview"; import { + LinkToFindings, ProvidersOverview, SkeletonProvidersOverview, } from "@/components/overview"; +import { ColumnNewFindingsToDate } from "@/components/overview/new-findings-table/table/column-new-findings-to-date"; +import { SkeletonTableNewFindings } from "@/components/overview/new-findings-table/table/skeleton-table-new-findings"; import { Header } from "@/components/ui"; +import { DataTable } from "@/components/ui/table"; +import { SearchParamsProps } from "@/types"; -export default function Home() { +export default function Home({ + searchParams, +}: { + searchParams: SearchParamsProps; +}) { + const searchParamsKey = JSON.stringify(searchParams || {}); return ( <>
- {/* Providers Overview */} -
- }> - - + {/* Providers Overview, Chart and New Findings Table */} +
+
+ }> + + +
+ + {/* Space for future chart */} +
+ {/* Future chart */} +
+ +
+ } + > + + +
- -
- -
@@ -34,9 +57,63 @@ export default function Home() { const SSRProvidersOverview = async () => { const providersOverview = await getProvidersOverview({}); - if (!providersOverview) { - return

There is no providers overview info available

; - } - - return ; + return ( + <> +

Providers Overview

+ + + ); +}; + +const SSRDataNewFindingsTable = async ({ + searchParams, +}: { + searchParams: SearchParamsProps; +}) => { + const page = parseInt(searchParams.page?.toString() || "1", 10); + const sort = searchParams.sort?.toString(); + + // Extract all filter parameters + const filters = Object.fromEntries( + Object.entries(searchParams).filter(([key]) => key.startsWith("filter[")), + ); + + const defaultFilters = + Object.keys(filters).length === 0 + ? { + "filter[severity]": "critical", + "filter[delta__in]": "new", + "filter[status__in]": "FAIL", + } + : {}; + + const finalFilters = { + ...defaultFilters, + ...filters, + } as Record; + + const query = finalFilters["filter[search]"]; + + const findingsData = await getFindings({ + query, + page, + sort, + filters: finalFilters, + }); + + return ( + <> +
+

+ New failing findings to date +

+ +
+ + + ); }; diff --git a/ui/components/overview/index.ts b/ui/components/overview/index.ts index eb393767f1..ed098ab865 100644 --- a/ui/components/overview/index.ts +++ b/ui/components/overview/index.ts @@ -1,3 +1,4 @@ export * from "./AttackSurface"; +export * from "./new-findings-table/link-to-findings/link-to-findings"; export * from "./provider-overview/provider-overview"; export * from "./provider-overview/skeleton-provider-overview"; diff --git a/ui/components/overview/new-findings-table/link-to-findings/link-to-findings.tsx b/ui/components/overview/new-findings-table/link-to-findings/link-to-findings.tsx new file mode 100644 index 0000000000..2cd675147a --- /dev/null +++ b/ui/components/overview/new-findings-table/link-to-findings/link-to-findings.tsx @@ -0,0 +1,21 @@ +"use client"; + +import { AddIcon } from "@/components/icons"; +import { CustomButton } from "@/components/ui/custom"; + +export const LinkToFindings = () => { + return ( +
+ } + > + Check out on findings + +
+ ); +}; diff --git a/ui/components/overview/new-findings-table/table/column-new-findings-to-date.tsx b/ui/components/overview/new-findings-table/table/column-new-findings-to-date.tsx new file mode 100644 index 0000000000..86556c6491 --- /dev/null +++ b/ui/components/overview/new-findings-table/table/column-new-findings-to-date.tsx @@ -0,0 +1,43 @@ +"use client"; + +import { ColumnDef } from "@tanstack/react-table"; + +import { DataTableColumnHeader, SeverityBadge } from "@/components/ui/table"; +import { FindingProps } from "@/types"; + +const getFindingsData = (row: { original: FindingProps }) => { + return row.original; +}; + +const getFindingsMetadata = (row: { original: FindingProps }) => { + return row.original.attributes.check_metadata; +}; + +export const ColumnNewFindingsToDate: ColumnDef[] = [ + { + accessorKey: "check", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const { checktitle } = getFindingsMetadata(row); + return

{checktitle}

; + }, + }, + { + accessorKey: "severity", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const { + attributes: { severity }, + } = getFindingsData(row); + return ; + }, + }, +]; diff --git a/ui/components/overview/new-findings-table/table/finding-detail.tsx b/ui/components/overview/new-findings-table/table/finding-detail.tsx new file mode 100644 index 0000000000..7f2a84e714 --- /dev/null +++ b/ui/components/overview/new-findings-table/table/finding-detail.tsx @@ -0,0 +1,219 @@ +"use client"; + +import { Snippet } from "@nextui-org/react"; +import Link from "next/link"; + +import { SnippetId } from "@/components/ui/entities"; +import { DateWithTime } from "@/components/ui/entities/date-with-time"; +import { SeverityBadge } from "@/components/ui/table/severity-badge"; +import { FindingProps } from "@/types"; + +export const FindingDetail = ({ + findingDetails, +}: { + findingDetails: FindingProps; +}) => { + const finding = findingDetails; + const attributes = finding.attributes; + const resource = finding.relationships.resource.attributes; + + const remediation = attributes.check_metadata.remediation; + + return ( +
+ {/* Header */} +
+
+

+ {attributes.check_metadata.checktitle} +

+

+ {resource.service} +

+
+
+ {attributes.status} +
+
+ + {/* Check Metadata */} +
+
+

+ Check Metadata +

+ +
+ {attributes.status === "FAIL" && ( + +

+ Risk +

+

+ {attributes.check_metadata.risk} +

+
+ )} + +
+

+ Description +

+

+ {attributes.check_metadata.description} +

+
+ +
+

+ Remediation +

+
+ {remediation.recommendation && ( + <> +

Recommendation:

+

{remediation.recommendation.text}

+ + Learn more + + + )} + {remediation.code && + Object.values(remediation.code).some(Boolean) && ( +
+

+ Check these links: +

+
+ {remediation.code.cli && ( +
+

CLI Command:

+ +

+ {remediation.code.cli} +

+
+
+ )} +
+ {Object.entries(remediation.code) + .filter(([key]) => key !== "cli") + .map(([key, value]) => + value ? ( + + {key === "other" + ? "External doc" + : key.charAt(0).toUpperCase() + + key.slice(1).toLowerCase()} + + ) : null, + )} +
+
+
+ )} +
+
+
+ + {/* Resources Section */} +
+

+ Resource Details +

+
+
+

+ Resource ID +

+ +

{resource.uid}

+
+
+
+

+ Resource Name +

+

+ {resource.name} +

+
+
+

+ Region +

+

+ {resource.region} +

+
+
+

+ Resource Type +

+

+ {resource.type} +

+
+
+

+ Severity +

+ +
+ {resource.tags && + Object.entries(resource.tags).map(([key, value]) => ( +
+

+ Tag: {key} +

+ +

{value}

+
+
+ ))} +
+
+

+ Inserted At +

+ +
+
+

+ Updated At +

+ +
+
+
+
+
+ ); +}; diff --git a/ui/components/overview/new-findings-table/table/index.ts b/ui/components/overview/new-findings-table/table/index.ts new file mode 100644 index 0000000000..5f8e56989e --- /dev/null +++ b/ui/components/overview/new-findings-table/table/index.ts @@ -0,0 +1,2 @@ +export * from "./column-new-findings-to-date"; +export * from "./skeleton-table-new-findings"; diff --git a/ui/components/overview/new-findings-table/table/skeleton-table-new-findings.tsx b/ui/components/overview/new-findings-table/table/skeleton-table-new-findings.tsx new file mode 100644 index 0000000000..6a24119681 --- /dev/null +++ b/ui/components/overview/new-findings-table/table/skeleton-table-new-findings.tsx @@ -0,0 +1,65 @@ +import { Card, Skeleton } from "@nextui-org/react"; +import React from "react"; + +export const SkeletonTableNewFindings = () => { + return ( + + {/* Table headers */} +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ + {/* Table body */} +
+ {[...Array(3)].map((_, index) => ( +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ ))} +
+
+ ); +}; diff --git a/ui/components/overview/provider-overview/provider-overview.tsx b/ui/components/overview/provider-overview/provider-overview.tsx index cd69f02595..a64d42c48b 100644 --- a/ui/components/overview/provider-overview/provider-overview.tsx +++ b/ui/components/overview/provider-overview/provider-overview.tsx @@ -1,6 +1,6 @@ "use client"; -import { Card, CardBody, CardHeader } from "@nextui-org/react"; +import { Card, CardBody } from "@nextui-org/react"; import { AddIcon } from "@/components/icons/Icons"; import { @@ -17,11 +17,6 @@ export const ProvidersOverview = ({ }: { providersOverview: ProviderOverviewProps; }) => { - console.log(providersOverview); - if (!providersOverview || !Array.isArray(providersOverview.data)) { - return

No provider data available

; - } - const calculatePassingPercentage = (pass: number, total: number) => total > 0 ? ((pass / total) * 100).toFixed(2) : "0.00"; @@ -47,11 +42,57 @@ export const ProvidersOverview = ({ { id: "kubernetes", name: "Kubernetes" }, ]; + if (!providersOverview || !Array.isArray(providersOverview.data)) { + return ( + + +
+
+ Provider + + Percent + Passing + + + Failing + Checks + + + Total + Resources + +
+ + {providers.map((providerTemplate) => ( +
+ + {renderProviderBadge(providerTemplate.id)} + + 0.00% + - + - +
+ ))} + +
+ + Total + + 0.00% + - + - +
+
+
+
+ ); + } + return ( - -

Providers Overview

-
diff --git a/ui/components/users/table/data-table-row-actions.tsx b/ui/components/users/table/data-table-row-actions.tsx index 61f4cdc390..24cba6006a 100644 --- a/ui/components/users/table/data-table-row-actions.tsx +++ b/ui/components/users/table/data-table-row-actions.tsx @@ -41,8 +41,8 @@ export function DataTableRowActions({ ({ isOpen={isDeleteOpen} onOpenChange={setIsDeleteOpen} title="Are you absolutely sure?" - description="This action cannot be undone. This will permanently delete your provider account and remove your data from the server." + description="This action cannot be undone. This will permanently delete your user account and remove your data from the server." > From afd152c073e8ab46832197aa0a7fc1d72c897c91 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Tue, 26 Nov 2024 16:26:06 +0100 Subject: [PATCH 07/56] feat(ui:cleaning): tweaks for Prowler v5 (#5913) --- ui/actions/auth/auth.ts | 9 ++++- ui/components/auth/oss/auth-form.tsx | 42 +++------------------- ui/components/ui/sidebar/sidebar-items.tsx | 34 +++++++++--------- ui/types/authFormSchema.ts | 6 ---- 4 files changed, 29 insertions(+), 62 deletions(-) diff --git a/ui/actions/auth/auth.ts b/ui/actions/auth/auth.ts index d5ec199b06..1bbc79b17e 100644 --- a/ui/actions/auth/auth.ts +++ b/ui/actions/auth/auth.ts @@ -89,7 +89,14 @@ export const createNewUser = async ( return parsedResponse; } catch (error) { - return { errors: [{ detail: "Network error or server is unreachable" }] }; + return { + errors: [ + { + source: { pointer: "" }, + detail: "Network error or server is unreachable", + }, + ], + }; } }; diff --git a/ui/components/auth/oss/auth-form.tsx b/ui/components/auth/oss/auth-form.tsx index 1f1ec4384f..340515c7ad 100644 --- a/ui/components/auth/oss/auth-form.tsx +++ b/ui/components/auth/oss/auth-form.tsx @@ -1,8 +1,7 @@ "use client"; import { zodResolver } from "@hookform/resolvers/zod"; -import { Icon } from "@iconify/react"; -import { Button, Checkbox, Divider, Link } from "@nextui-org/react"; +import { Checkbox, Link } from "@nextui-org/react"; import { useRouter } from "next/navigation"; import { useForm } from "react-hook-form"; import { z } from "zod"; @@ -12,12 +11,7 @@ import { NotificationIcon, ProwlerExtended } from "@/components/icons"; import { ThemeSwitch } from "@/components/ThemeSwitch"; import { useToast } from "@/components/ui"; import { CustomButton, CustomInput } from "@/components/ui/custom"; -import { - Form, - FormControl, - FormField, - FormMessage, -} from "@/components/ui/form"; +import { Form } from "@/components/ui/form"; import { ApiError, authFormSchema } from "@/types"; export const AuthForm = ({ @@ -38,7 +32,6 @@ export const AuthForm = ({ ...(type === "sign-up" && { name: "", company: "", - termsAndConditions: false, confirmPassword: "", ...(invitationToken && { invitationToken }), }), @@ -211,33 +204,6 @@ export const AuthForm = ({ isInvalid={!!form.formState.errors.invitationToken} /> )} - ( - <> - - field.onChange(e.target.checked)} - > - I agree with the  - - Terms - -   and  - - Privacy Policy - - - - - - )} - /> )} @@ -269,7 +235,7 @@ export const AuthForm = ({ - {type === "sign-in" && ( + {/* {type === "sign-in" && ( <>
@@ -299,7 +265,7 @@ export const AuthForm = ({
- )} + )} */} {type === "sign-in" ? (

Need to create an account?  diff --git a/ui/components/ui/sidebar/sidebar-items.tsx b/ui/components/ui/sidebar/sidebar-items.tsx index a4fae9eab4..e7b2fd0072 100644 --- a/ui/components/ui/sidebar/sidebar-items.tsx +++ b/ui/components/ui/sidebar/sidebar-items.tsx @@ -142,18 +142,18 @@ export const sectionItems: SidebarItem[] = [ href: "/compliance", icon: "fluent-mdl2:compliance-audit", title: "Compliance", - endContent: ( - - New - - ), - }, - { - key: "services", - href: "/services", - icon: "material-symbols:linked-services-outline", - title: "Services", + // endContent: ( + // + // New + // + // ), }, + // { + // key: "services", + // href: "/services", + // icon: "material-symbols:linked-services-outline", + // title: "Services", + // }, ], }, @@ -167,12 +167,12 @@ export const sectionItems: SidebarItem[] = [ icon: "fluent:cloud-sync-24-regular", title: "Providers", }, - { - key: "integrations", - href: "/integrations", - icon: "tabler:puzzle", - title: "Integrations", - }, + // { + // key: "integrations", + // href: "/integrations", + // icon: "tabler:puzzle", + // title: "Integrations", + // }, ], }, ]; diff --git a/ui/types/authFormSchema.ts b/ui/types/authFormSchema.ts index a7219a69fa..ac0fc46dcf 100644 --- a/ui/types/authFormSchema.ts +++ b/ui/types/authFormSchema.ts @@ -23,12 +23,6 @@ export const authFormSchema = (type: string) => }), invitationToken: type === "sign-in" ? z.string().optional() : z.string().optional(), - termsAndConditions: - type === "sign-in" - ? z.boolean().optional() - : z.boolean().refine((value) => value === true, { - message: "You must accept the terms and conditions.", - }), // Fields for Sign In and Sign Up email: z.string().email(), From 4a8150d6136e0d3f7c2ba6c8bf8aa26aa668f277 Mon Sep 17 00:00:00 2001 From: Prowler Bot Date: Wed, 27 Nov 2024 13:51:19 +0100 Subject: [PATCH 08/56] chore(regions_update): Changes in regions for AWS services (#5922) Co-authored-by: sergargar <38561120+sergargar@users.noreply.github.com> --- .../providers/aws/aws_regions_by_service.json | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/prowler/providers/aws/aws_regions_by_service.json b/prowler/providers/aws/aws_regions_by_service.json index fed0f3a69b..f8ec725446 100644 --- a/prowler/providers/aws/aws_regions_by_service.json +++ b/prowler/providers/aws/aws_regions_by_service.json @@ -7404,25 +7404,6 @@ "aws-us-gov": [] } }, - "nimble": { - "regions": { - "aws": [ - "ap-northeast-1", - "ap-southeast-1", - "ap-southeast-2", - "ca-central-1", - "eu-central-1", - "eu-north-1", - "eu-west-1", - "eu-west-2", - "us-east-1", - "us-east-2", - "us-west-2" - ], - "aws-cn": [], - "aws-us-gov": [] - } - }, "notifications": { "regions": { "aws": [ From 677e20a1a4ea5d89a29ca4187ea3dfa4280baed5 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Wed, 27 Nov 2024 13:53:16 +0100 Subject: [PATCH 09/56] feat(ui:overview) overview findings by status and severity (#5925) --- ui/actions/overview/overview.ts | 96 ++++++++- ui/app/(prowler)/compliance/page.tsx | 105 +++++++--- ui/app/(prowler)/page.tsx | 118 ++++++++--- ui/components/charts/SeverityChart.tsx | 76 ------- ui/components/charts/StatusChart.tsx | 148 ------------- ui/components/charts/index.ts | 2 - ui/components/filters/custom-date-picker.tsx | 2 + .../filters/custom-region-selection.tsx | 33 ++- ui/components/filters/custom-search-input.tsx | 3 +- .../filters/custom-select-provider.tsx | 3 +- .../findings-by-severity-chart.tsx | 108 ++++++++++ .../skeleton-findings-severity-chart.tsx | 62 ++++++ .../findings-by-status-chart.tsx | 196 ++++++++++++++++++ .../skeleton-findings-status-chart.tsx | 54 +++++ ui/components/overview/index.ts | 4 + .../link-to-findings/link-to-findings.tsx | 2 +- ui/types/components.ts | 35 ++++ 17 files changed, 754 insertions(+), 293 deletions(-) delete mode 100644 ui/components/charts/SeverityChart.tsx delete mode 100644 ui/components/charts/StatusChart.tsx delete mode 100644 ui/components/charts/index.ts create mode 100644 ui/components/overview/findings-by-severity-chart/findings-by-severity-chart.tsx create mode 100644 ui/components/overview/findings-by-severity-chart/skeleton-findings-severity-chart.tsx create mode 100644 ui/components/overview/findings-by-status-chart/findings-by-status-chart.tsx create mode 100644 ui/components/overview/findings-by-status-chart/skeleton-findings-status-chart.tsx diff --git a/ui/actions/overview/overview.ts b/ui/actions/overview/overview.ts index 8d83ab7088..2095a9f32f 100644 --- a/ui/actions/overview/overview.ts +++ b/ui/actions/overview/overview.ts @@ -39,7 +39,7 @@ export const getProvidersOverview = async ({ const data = await response.json(); const parsedData = parseStringify(data); - revalidatePath("/providers-overview"); + revalidatePath("/"); return parsedData; } catch (error) { // eslint-disable-next-line no-console @@ -47,3 +47,97 @@ export const getProvidersOverview = async ({ return undefined; } }; + +export const getFindingsByStatus = async ({ + page = 1, + query = "", + sort = "", + filters = {}, +}) => { + const session = await auth(); + + if (isNaN(Number(page)) || page < 1) redirect("/"); + + const keyServer = process.env.API_BASE_URL; + const url = new URL(`${keyServer}/overviews/findings`); + + if (page) url.searchParams.append("page[number]", page.toString()); + if (query) url.searchParams.append("filter[search]", query); + if (sort) url.searchParams.append("sort", sort); + + // Handle multiple filters + Object.entries(filters).forEach(([key, value]) => { + if (key !== "filter[search]") { + url.searchParams.append(key, String(value)); + } + }); + + try { + const response = await fetch(url.toString(), { + headers: { + Accept: "application/vnd.api+json", + Authorization: `Bearer ${session?.accessToken}`, + }, + }); + + if (!response.ok) { + throw new Error(`Failed to fetch findings severity: ${response.status}`); + } + + const data = await response.json(); + const parsedData = parseStringify(data); + revalidatePath("/"); + return parsedData; + } catch (error) { + // eslint-disable-next-line no-console + console.error("Error fetching findings severity overview:", error); + return undefined; + } +}; + +export const getFindingsBySeverity = async ({ + page = 1, + query = "", + sort = "", + filters = {}, +}) => { + const session = await auth(); + + if (isNaN(Number(page)) || page < 1) redirect("/"); + + const keyServer = process.env.API_BASE_URL; + const url = new URL(`${keyServer}/overviews/findings_severity`); + + if (page) url.searchParams.append("page[number]", page.toString()); + if (query) url.searchParams.append("filter[search]", query); + if (sort) url.searchParams.append("sort", sort); + + // Handle multiple filters + Object.entries(filters).forEach(([key, value]) => { + if (key !== "filter[search]") { + url.searchParams.append(key, String(value)); + } + }); + + try { + const response = await fetch(url.toString(), { + headers: { + Accept: "application/vnd.api+json", + Authorization: `Bearer ${session?.accessToken}`, + }, + }); + + if (!response.ok) { + throw new Error(`Failed to fetch findings severity: ${response.status}`); + } + + const data = await response.json(); + const parsedData = parseStringify(data); + revalidatePath("/"); + return parsedData; + } catch (error) { + // eslint-disable-next-line no-console + console.error("Error fetching findings severity overview:", error); + return undefined; + } +}; diff --git a/ui/app/(prowler)/compliance/page.tsx b/ui/app/(prowler)/compliance/page.tsx index 38c9800467..1bc58ad741 100644 --- a/ui/app/(prowler)/compliance/page.tsx +++ b/ui/app/(prowler)/compliance/page.tsx @@ -16,44 +16,70 @@ export default async function Compliance({ }: { searchParams: SearchParamsProps; }) { - const scansData = await getScans({}); - const scanList = scansData?.data - .filter( - (scan: any) => - scan.attributes.state === "completed" && - scan.attributes.progress === 100, - ) - .map((scan: any) => ({ - id: scan.id, - name: scan.attributes.name || "Unnamed Scan", - state: scan.attributes.state, - progress: scan.attributes.progress, - })); + let scansData; + let scanList: { + id: string; + name: string; + state: string; + progress: number; + }[] = []; - const selectedScanId = searchParams.scanId || scanList[0]?.id; + try { + scansData = await getScans({}); + scanList = + scansData?.data + ?.filter( + (scan: any) => + scan.attributes.state === "completed" && + scan.attributes.progress === 100, + ) + .map((scan: any) => ({ + id: scan.id, + name: scan.attributes.name || "Unnamed Scan", + state: scan.attributes.state, + progress: scan.attributes.progress, + })) || []; + } catch (error) { + console.error("Error fetching scans data:", error); + } + + const selectedScanId = searchParams.scanId || scanList[0]?.id || null; + + // If there are no scans available, return a message + if (!selectedScanId) { + return ( +

+
No scans available to select.
+
+ ); + } // Fetch compliance data for regions - const compliancesData = await getCompliancesOverview({ - scanId: selectedScanId, - }); - - // Extract unique regions - const regions = compliancesData?.data - ? Array.from( - new Set( - compliancesData.data.map( - (compliance: ComplianceOverviewData) => - compliance.attributes.region as string, + let compliancesData; + let regions: string[] = []; + try { + compliancesData = await getCompliancesOverview({ + scanId: selectedScanId as string, + }); + regions = compliancesData?.data + ? Array.from( + new Set( + compliancesData.data.map( + (compliance: ComplianceOverviewData) => + compliance.attributes.region as string, + ), ), - ), - ) - : []; + ) + : []; + } catch (error) { + console.error("Error fetching compliance data:", error); + } return ( <>
- + }> @@ -68,14 +94,25 @@ const SSRComplianceGrid = async ({ searchParams: SearchParamsProps; }) => { const scanId = searchParams.scanId?.toString() || ""; - const regionFilter = searchParams["filter[region__in]"]?.toString() || ""; // Fetch compliance data - const compliancesData = await getCompliancesOverview({ - scanId, - region: regionFilter, - }); + let compliancesData; + try { + compliancesData = await getCompliancesOverview({ + scanId, + region: regionFilter, + }); + } catch (error) { + console.error("Error fetching compliances overview:", error); + return ( +
+
+ Failed to load compliance data. Please try again later. +
+
+ ); + } // Check if the response contains no data if (!compliancesData || compliancesData?.data?.length === 0) { diff --git a/ui/app/(prowler)/page.tsx b/ui/app/(prowler)/page.tsx index df260ec8e3..fedfdf672c 100644 --- a/ui/app/(prowler)/page.tsx +++ b/ui/app/(prowler)/page.tsx @@ -2,10 +2,19 @@ import { Spacer } from "@nextui-org/react"; import { Suspense } from "react"; import { getFindings } from "@/actions/findings/findings"; -import { getProvidersOverview } from "@/actions/overview/overview"; import { + getFindingsBySeverity, + getFindingsByStatus, + getProvidersOverview, +} from "@/actions/overview/overview"; +import { FilterControls } from "@/components/filters"; +import { + FindingsBySeverityChart, + FindingsByStatusChart, LinkToFindings, ProvidersOverview, + SkeletonFindingsBySeverityChart, + SkeletonFindingsByStatusChart, SkeletonProvidersOverview, } from "@/components/overview"; import { ColumnNewFindingsToDate } from "@/components/overview/new-findings-table/table/column-new-findings-to-date"; @@ -24,9 +33,9 @@ export default function Home({ <>
+
- {/* Providers Overview, Chart and New Findings Table */}
}> @@ -34,9 +43,18 @@ export default function Home({
- {/* Space for future chart */} -
- {/* Future chart */} +
+
+ }> + + +
+ +
+ }> + + +
@@ -44,7 +62,7 @@ export default function Home({ key={searchParamsKey} fallback={} > - +
@@ -65,34 +83,80 @@ const SSRProvidersOverview = async () => { ); }; -const SSRDataNewFindingsTable = async ({ +const SSRFindingsByStatus = async ({ searchParams, }: { - searchParams: SearchParamsProps; + searchParams: SearchParamsProps | undefined | null; }) => { - const page = parseInt(searchParams.page?.toString() || "1", 10); - const sort = searchParams.sort?.toString(); + const filters = searchParams + ? Object.fromEntries( + Object.entries(searchParams).filter(([key]) => + key.startsWith("filter["), + ), + ) + : {}; + + const findingsByStatus = await getFindingsByStatus({ filters }); + + return ( + <> +

Findings by Status

+ + + ); +}; + +const SSRFindingsBySeverity = async ({ + searchParams, +}: { + searchParams: SearchParamsProps | undefined | null; +}) => { + const filters = searchParams + ? Object.fromEntries( + Object.entries(searchParams).filter(([key]) => + key.startsWith("filter["), + ), + ) + : {}; + + const findingsBySeverity = await getFindingsBySeverity({ filters }); + + return ( + <> +

Findings by Severity

+ + + ); +}; + +const SSRDataNewFindingsTable = async () => { + // Temporarily disabled search params handling + // const page = parseInt(searchParams.page?.toString() || "1", 10); + // const sort = searchParams.sort?.toString(); + const page = 1; + const sort = undefined; // Extract all filter parameters - const filters = Object.fromEntries( - Object.entries(searchParams).filter(([key]) => key.startsWith("filter[")), - ); + // const filters = Object.fromEntries( + // Object.entries(searchParams).filter(([key]) => key.startsWith("filter[")), + // ); - const defaultFilters = - Object.keys(filters).length === 0 - ? { - "filter[severity]": "critical", - "filter[delta__in]": "new", - "filter[status__in]": "FAIL", - } - : {}; + // const defaultFilters = + // Object.keys(filters).length === 0 + // ? { + const defaultFilters = { + "filter[delta__in]": "new", + "filter[status__in]": "FAIL", + }; + // : {}; const finalFilters = { ...defaultFilters, - ...filters, + // ...filters, // Temporarily disabled additional filters } as Record; - const query = finalFilters["filter[search]"]; + // const query = finalFilters["filter[search]"]; + const query = undefined; const findingsData = await getFindings({ query, @@ -103,16 +167,18 @@ const SSRDataNewFindingsTable = async ({ return ( <> -
+

New failing findings to date

- +
+ +
); diff --git a/ui/components/charts/SeverityChart.tsx b/ui/components/charts/SeverityChart.tsx deleted file mode 100644 index 4c3060c8bd..0000000000 --- a/ui/components/charts/SeverityChart.tsx +++ /dev/null @@ -1,76 +0,0 @@ -"use client"; - -import { Bar, BarChart, LabelList, XAxis, YAxis } from "recharts"; - -import { - ChartConfig, - ChartContainer, - ChartTooltip, - ChartTooltipContent, -} from "@/components/ui/chart/Chart"; - -const chartData = [ - { severity: "critical", findings: 32, fill: "var(--color-critical)" }, - { severity: "high", findings: 78, fill: "var(--color-high)" }, - { severity: "medium", findings: 117, fill: "var(--color-medium)" }, - { severity: "low", findings: 39, fill: "var(--color-low)" }, -]; - -const chartConfig = { - findings: { - label: "Findings", - }, - critical: { - label: "Critical", - color: "hsl(var(--chart-critical))", - }, - high: { - label: "High", - color: "hsl(var(--chart-fail))", - }, - medium: { - label: "Medium", - color: "hsl(var(--chart-medium))", - }, - low: { - label: "Low", - color: "hsl(var(--chart-low))", - }, -} satisfies ChartConfig; - -export const SeverityChart = () => { - return ( -
- - - - chartConfig[value as keyof typeof chartConfig]?.label - } - /> - - - - } - /> - - - - - - -
- ); -}; diff --git a/ui/components/charts/StatusChart.tsx b/ui/components/charts/StatusChart.tsx deleted file mode 100644 index d906db502e..0000000000 --- a/ui/components/charts/StatusChart.tsx +++ /dev/null @@ -1,148 +0,0 @@ -"use client"; - -import { Chip, Divider, Spacer } from "@nextui-org/react"; -import { TrendingUp } from "lucide-react"; -import * as React from "react"; -import { Label, Pie, PieChart } from "recharts"; - -import { NotificationIcon, SuccessIcon } from "../icons"; -import { - ChartConfig, - ChartContainer, - ChartTooltip, - ChartTooltipContent, -} from "../ui"; - -const calculatePercent = ( - chartData: { findings: string; number: number; fill: string }[], -) => { - const total = chartData.reduce((sum, item) => sum + item.number, 0); - - return chartData.map((item) => ({ - ...item, - percent: Math.round((item.number / total) * 100) + "%", - })); -}; - -const chartData = [ - { - findings: "Success", - number: 436, - fill: "var(--color-success)", - }, - { findings: "Fail", number: 293, fill: "var(--color-fail)" }, -]; - -const updatedChartData = calculatePercent(chartData); - -const chartConfig = { - number: { - label: "Findings", - }, - success: { - label: "Success", - color: "hsl(var(--chart-success))", - }, - fail: { - label: "Fail", - color: "hsl(var(--chart-fail))", - }, -} satisfies ChartConfig; - -export function StatusChart() { - const totalVisitors = React.useMemo(() => { - return chartData.reduce((acc, curr) => acc + curr.number, 0); - }, []); - - return ( -
- - - } /> - - - - -
-
- } - color="success" - radius="lg" - size="md" - > - {chartData[0].number} - - - {updatedChartData[0].percent} - -
-
- No change from last scan -
- -
-
- } - color="danger" - radius="lg" - size="md" - > - {chartData[1].number} - - - {updatedChartData[1].percent} - -
-
- +2 findings from last scan -
-
-
-
- ); -} diff --git a/ui/components/charts/index.ts b/ui/components/charts/index.ts deleted file mode 100644 index b4d3debb45..0000000000 --- a/ui/components/charts/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./SeverityChart"; -export * from "./StatusChart"; diff --git a/ui/components/filters/custom-date-picker.tsx b/ui/components/filters/custom-date-picker.tsx index 2a982c1781..21bb209282 100644 --- a/ui/components/filters/custom-date-picker.tsx +++ b/ui/components/filters/custom-date-picker.tsx @@ -60,6 +60,8 @@ export const CustomDatePicker = () => {
{ +export const CustomRegionSelection: React.FC = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + + // Memoize selected keys based on the URL + const selectedKeys = useMemo(() => { + const params = searchParams.get("filter[regions]"); + return params ? params.split(",") : []; + }, [searchParams]); + + const applyRegionFilter = useCallback( + (values: string[]) => { + const params = new URLSearchParams(searchParams.toString()); + if (values.length > 0) { + params.set("filter[regions]", values.join(",")); + } else { + params.delete("filter[regions]"); + } + router.push(`?${params.toString()}`, { scroll: false }); + }, + [router, searchParams], + ); + return ( ); diff --git a/ui/components/filters/custom-search-input.tsx b/ui/components/filters/custom-search-input.tsx index f3d9808125..a5f4519877 100644 --- a/ui/components/filters/custom-search-input.tsx +++ b/ui/components/filters/custom-search-input.tsx @@ -39,8 +39,9 @@ export const CustomSearchInput: React.FC = () => { } onChange={(e) => { diff --git a/ui/components/filters/custom-select-provider.tsx b/ui/components/filters/custom-select-provider.tsx index d2a419bbbb..907e8be10b 100644 --- a/ui/components/filters/custom-select-provider.tsx +++ b/ui/components/filters/custom-select-provider.tsx @@ -66,7 +66,8 @@ export const CustomSelectProvider: React.FC = () => { items={dataInputsProvider} aria-label="Select a Provider" placeholder="Select a provider" - labelPlacement="outside" + label="Provider" + labelPlacement="inside" size="sm" onChange={(e) => { const value = e.target.value; diff --git a/ui/components/overview/findings-by-severity-chart/findings-by-severity-chart.tsx b/ui/components/overview/findings-by-severity-chart/findings-by-severity-chart.tsx new file mode 100644 index 0000000000..7e590fb0d1 --- /dev/null +++ b/ui/components/overview/findings-by-severity-chart/findings-by-severity-chart.tsx @@ -0,0 +1,108 @@ +"use client"; + +import { Card, CardBody } from "@nextui-org/react"; +import { Bar, BarChart, LabelList, XAxis, YAxis } from "recharts"; + +import { + ChartConfig, + ChartContainer, + ChartTooltip, + ChartTooltipContent, +} from "@/components/ui/chart/Chart"; +import { FindingsSeverityOverview } from "@/types/components"; + +const chartConfig = { + critical: { + label: "Critical", + color: "hsl(var(--chart-critical))", + }, + high: { + label: "High", + color: "hsl(var(--chart-fail))", + }, + medium: { + label: "Medium", + color: "hsl(var(--chart-medium))", + }, + low: { + label: "Low", + color: "hsl(var(--chart-low))", + }, + informational: { + label: "Informational", + color: "hsl(var(--chart-informational))", + }, +} satisfies ChartConfig; + +export const FindingsBySeverityChart = ({ + findingsBySeverity, +}: { + findingsBySeverity: FindingsSeverityOverview; +}) => { + const defaultAttributes = { + critical: 0, + high: 0, + medium: 0, + low: 0, + informational: 0, + }; + + const attributes = findingsBySeverity?.data?.attributes || defaultAttributes; + + const chartData = Object.entries(attributes).map(([severity, findings]) => ({ + severity, + findings, + fill: chartConfig[severity as keyof typeof chartConfig]?.color, + })); + + return ( + + +
+ + + + chartConfig[value as keyof typeof chartConfig]?.label + } + /> + + + + } + /> + + + + + +
+
+
+ ); +}; diff --git a/ui/components/overview/findings-by-severity-chart/skeleton-findings-severity-chart.tsx b/ui/components/overview/findings-by-severity-chart/skeleton-findings-severity-chart.tsx new file mode 100644 index 0000000000..e3eb51b49f --- /dev/null +++ b/ui/components/overview/findings-by-severity-chart/skeleton-findings-severity-chart.tsx @@ -0,0 +1,62 @@ +import { Card, CardBody, CardHeader, Skeleton } from "@nextui-org/react"; + +export const SkeletonFindingsBySeverityChart = () => { + return ( + + + +
+
+
+ +
+ {/* Critical */} +
+ +
+
+ +
+
+
+ {/* High */} +
+ +
+
+ +
+
+
+ {/* Medium */} +
+ +
+
+ +
+
+
+ {/* Low */} +
+ +
+
+ +
+
+
+ {/* Informational */} +
+ +
+
+ +
+
+
+
+
+
+ ); +}; diff --git a/ui/components/overview/findings-by-status-chart/findings-by-status-chart.tsx b/ui/components/overview/findings-by-status-chart/findings-by-status-chart.tsx new file mode 100644 index 0000000000..0f6151ce96 --- /dev/null +++ b/ui/components/overview/findings-by-status-chart/findings-by-status-chart.tsx @@ -0,0 +1,196 @@ +"use client"; + +import { Card, CardBody } from "@nextui-org/react"; +import { Chip } from "@nextui-org/react"; +import { TrendingUp } from "lucide-react"; +import React, { useMemo } from "react"; +import { Label, Pie, PieChart } from "recharts"; + +import { NotificationIcon, SuccessIcon } from "@/components/icons"; +import { + ChartConfig, + ChartContainer, + ChartTooltip, + ChartTooltipContent, +} from "@/components/ui/chart/Chart"; + +const calculatePercent = ( + chartData: { findings: string; number: number; fill: string }[], +) => { + const total = chartData.reduce((sum, item) => sum + item.number, 0); + + return chartData.map((item) => ({ + ...item, + percent: total > 0 ? Math.round((item.number / total) * 100) + "%" : "0%", + })); +}; + +interface FindingsByStatusChartProps { + findingsByStatus: { + data: { + attributes: { + fail: number; + pass: number; + pass_new: number; + fail_new: number; + total: number; + }; + }; + }; +} + +const chartConfig = { + number: { + label: "Findings", + }, + success: { + label: "Success", + color: "hsl(var(--chart-success))", + }, + fail: { + label: "Fail", + color: "hsl(var(--chart-fail))", + }, +} satisfies ChartConfig; + +export const FindingsByStatusChart: React.FC = ({ + findingsByStatus, +}) => { + const { + fail = 0, + pass = 0, + pass_new = 0, + fail_new = 0, + } = findingsByStatus?.data?.attributes || {}; + const chartData = [ + { + findings: "Success", + number: pass, + fill: "var(--color-success)", + }, + { + findings: "Fail", + number: fail, + fill: "var(--color-fail)", + }, + ]; + + const updatedChartData = calculatePercent(chartData); + + const totalFindings = useMemo( + () => chartData.reduce((acc, curr) => acc + curr.number, 0), + [chartData], + ); + + const emptyChartData = [ + { + findings: "Empty", + number: 1, + fill: "hsl(var(--nextui-default-200))", + }, + ]; + + return ( + + +
+ + + } /> + 0 ? chartData : emptyChartData} + dataKey="number" + nameKey="findings" + innerRadius={40} + strokeWidth={35} + > + + + +
+
+
+ } + color="success" + radius="lg" + size="md" + > + {chartData[0].number} + + {updatedChartData[0].percent} +
+
+ {pass_new > 0 ? ( + <> + +{pass_new} pass findings from last day{" "} + + + ) : pass_new < 0 ? ( + <>{pass_new} pass findings from last day + ) : ( + "No change from last day" + )} +
+
+
+
+ } + color="danger" + radius="lg" + size="md" + > + {chartData[1].number} + + {updatedChartData[1].percent} +
+
+ +{fail_new} fail findings from last day{" "} + +
+
+
+
+
+
+ ); +}; diff --git a/ui/components/overview/findings-by-status-chart/skeleton-findings-status-chart.tsx b/ui/components/overview/findings-by-status-chart/skeleton-findings-status-chart.tsx new file mode 100644 index 0000000000..f4b8a9d12b --- /dev/null +++ b/ui/components/overview/findings-by-status-chart/skeleton-findings-status-chart.tsx @@ -0,0 +1,54 @@ +import { Card, CardBody, CardHeader, Skeleton } from "@nextui-org/react"; + +export const SkeletonFindingsByStatusChart = () => { + return ( + + + +
+
+
+ +
+ {/* Circle Chart Skeleton */} + +
+
+ + {/* Text Details Skeleton */} +
+ {/* Pass Findings */} +
+
+ +
+
+ +
+
+
+ +
+
+
+ + {/* Fail Findings */} +
+
+ +
+
+ +
+
+
+ +
+
+
+
+
+
+
+ ); +}; diff --git a/ui/components/overview/index.ts b/ui/components/overview/index.ts index ed098ab865..e2beebb034 100644 --- a/ui/components/overview/index.ts +++ b/ui/components/overview/index.ts @@ -1,4 +1,8 @@ export * from "./AttackSurface"; +export * from "./findings-by-severity-chart/findings-by-severity-chart"; +export * from "./findings-by-severity-chart/skeleton-findings-severity-chart"; +export * from "./findings-by-status-chart/findings-by-status-chart"; +export * from "./findings-by-status-chart/skeleton-findings-status-chart"; export * from "./new-findings-table/link-to-findings/link-to-findings"; export * from "./provider-overview/provider-overview"; export * from "./provider-overview/skeleton-provider-overview"; diff --git a/ui/components/overview/new-findings-table/link-to-findings/link-to-findings.tsx b/ui/components/overview/new-findings-table/link-to-findings/link-to-findings.tsx index 2cd675147a..533b32cb01 100644 --- a/ui/components/overview/new-findings-table/link-to-findings/link-to-findings.tsx +++ b/ui/components/overview/new-findings-table/link-to-findings/link-to-findings.tsx @@ -7,7 +7,7 @@ export const LinkToFindings = () => { return (
Date: Wed, 27 Nov 2024 09:07:21 -0400 Subject: [PATCH 10/56] chore(deps): bump botocore from 1.35.69 to 1.35.70 (#5918) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- poetry.lock | 8 ++++---- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/poetry.lock b/poetry.lock index d7b8fca61f..3131386631 100644 --- a/poetry.lock +++ b/poetry.lock @@ -794,13 +794,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.35.69" +version = "1.35.70" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">=3.8" files = [ - {file = "botocore-1.35.69-py3-none-any.whl", hash = "sha256:cad8d9305f873404eee4b197d84e60a40975d43cbe1ab63abe893420ddfe6e3c"}, - {file = "botocore-1.35.69.tar.gz", hash = "sha256:f9f23dd76fb247d9b0e8d411d2995e6f847fc451c026f1e58e300f815b0b36eb"}, + {file = "botocore-1.35.70-py3-none-any.whl", hash = "sha256:ba8a4797cf7c5d9c237e67a62692f5146e895613fd3e6a43b00b66f3a8c7fc73"}, + {file = "botocore-1.35.70.tar.gz", hash = "sha256:18d1bb505722d9efd50c50719ed8de7284bfe6d3908a9e08756a7646e549da21"}, ] [package.dependencies] @@ -5194,4 +5194,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.13" -content-hash = "951867954f393582007c653ec643754099a8b5df909a81bd6f0cd5c8ec3326a9" +content-hash = "eb5ea3d18ceee3e84c3b887c377cac49ee6a2af876ca20257930dd364234f66b" diff --git a/pyproject.toml b/pyproject.toml index c71a0485cd..3aab82a665 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,7 +49,7 @@ azure-mgmt-subscription = "3.1.1" azure-mgmt-web = "7.3.1" azure-storage-blob = "12.24.0" boto3 = "1.35.66" -botocore = "1.35.69" +botocore = "1.35.70" colorama = "0.4.6" cryptography = "43.0.1" dash = "2.18.2" From f9864eeda0a401edf341f9567ff8c82392b8425c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 27 Nov 2024 10:16:36 -0400 Subject: [PATCH 11/56] chore(deps): bump boto3 from 1.35.66 to 1.35.70 (#5929) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- poetry.lock | 10 +++++----- pyproject.toml | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/poetry.lock b/poetry.lock index 3131386631..3bfa70b6cf 100644 --- a/poetry.lock +++ b/poetry.lock @@ -775,17 +775,17 @@ files = [ [[package]] name = "boto3" -version = "1.35.66" +version = "1.35.70" description = "The AWS SDK for Python" optional = false python-versions = ">=3.8" files = [ - {file = "boto3-1.35.66-py3-none-any.whl", hash = "sha256:09a610f8cf4d3c22d4ca69c1f89079e3a1c82805ce94fa0eb4ecdd4d2ba6c4bc"}, - {file = "boto3-1.35.66.tar.gz", hash = "sha256:c392b9168b65e9c23483eaccb5b68d1f960232d7f967a1e00a045ba065ce050d"}, + {file = "boto3-1.35.70-py3-none-any.whl", hash = "sha256:ca385708f83f01b3f27d9d675880d2458cb3b40ed1e25da688f551454ed0c112"}, + {file = "boto3-1.35.70.tar.gz", hash = "sha256:121dce8c7102eea6a6047d46bcd74e8a24dac793a4a3857de4f4bad9c12566fd"}, ] [package.dependencies] -botocore = ">=1.35.66,<1.36.0" +botocore = ">=1.35.70,<1.36.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.10.0,<0.11.0" @@ -5194,4 +5194,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.13" -content-hash = "eb5ea3d18ceee3e84c3b887c377cac49ee6a2af876ca20257930dd364234f66b" +content-hash = "6a868e7040647c561274f7bb54dd5a899690d2b8dc658559c9de7e175debee6e" diff --git a/pyproject.toml b/pyproject.toml index 3aab82a665..582896a6c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,7 @@ azure-mgmt-storage = "21.2.1" azure-mgmt-subscription = "3.1.1" azure-mgmt-web = "7.3.1" azure-storage-blob = "12.24.0" -boto3 = "1.35.66" +boto3 = "1.35.70" botocore = "1.35.70" colorama = "0.4.6" cryptography = "43.0.1" From f576b24fc8bba38322020bb35a9d9fe667795d5b Mon Sep 17 00:00:00 2001 From: Pepe Fagoaga Date: Wed, 27 Nov 2024 15:32:57 +0100 Subject: [PATCH 12/56] fix(list_by_service): execute lambda if requested (#5930) --- prowler/lib/check/models.py | 5 ++- tests/lib/check/models_test.py | 69 ++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/prowler/lib/check/models.py b/prowler/lib/check/models.py index 446440b6aa..b060c58e86 100644 --- a/prowler/lib/check/models.py +++ b/prowler/lib/check/models.py @@ -322,8 +322,9 @@ class CheckMetadata(BaseModel): checks = set() if service: - if service == "lambda": - service = "awslambda" + # This is a special case for the AWS provider since `lambda` is a reserved keyword in Python + if service == "awslambda": + service = "lambda" checks = { check_name for check_name, check_metadata in bulk_checks_metadata.items() diff --git a/tests/lib/check/models_test.py b/tests/lib/check/models_test.py index 7cf470c7e6..414de90288 100644 --- a/tests/lib/check/models_test.py +++ b/tests/lib/check/models_test.py @@ -32,6 +32,35 @@ mock_metadata = CheckMetadata( Compliance=[], ) +mock_metadata_lambda = CheckMetadata( + Provider="aws", + CheckID="awslambda_function_url_public", + CheckTitle="Check 1", + CheckType=["type1"], + ServiceName="lambda", + SubServiceName="subservice1", + ResourceIdTemplate="template1", + Severity="high", + ResourceType="resource1", + Description="Description 1", + Risk="risk1", + RelatedUrl="url1", + Remediation={ + "Code": { + "CLI": "cli1", + "NativeIaC": "native1", + "Other": "other1", + "Terraform": "terraform1", + }, + "Recommendation": {"Text": "text1", "Url": "url1"}, + }, + Categories=["categoryone"], + DependsOn=["dependency1"], + RelatedTo=["related1"], + Notes="notes1", + Compliance=[], +) + class TestCheckMetada: @@ -188,6 +217,46 @@ class TestCheckMetada: # Assertions assert result == {"accessanalyzer_enabled"} + @mock.patch("prowler.lib.check.models.load_check_metadata") + @mock.patch("prowler.lib.check.models.recover_checks_from_provider") + def test_list_by_service_lambda(self, mock_recover_checks, mock_load_metadata): + # Mock the return value of recover_checks_from_provider + mock_recover_checks.return_value = [ + ("awslambda_function_url_public", "/path/to/awslambda_function_url_public") + ] + + # Mock the return value of load_check_metadata + mock_load_metadata.return_value = mock_metadata_lambda + + bulk_metadata = CheckMetadata.get_bulk(provider="aws") + + result = CheckMetadata.list( + bulk_checks_metadata=bulk_metadata, service="lambda" + ) + + # Assertions + assert result == {"awslambda_function_url_public"} + + @mock.patch("prowler.lib.check.models.load_check_metadata") + @mock.patch("prowler.lib.check.models.recover_checks_from_provider") + def test_list_by_service_awslambda(self, mock_recover_checks, mock_load_metadata): + # Mock the return value of recover_checks_from_provider + mock_recover_checks.return_value = [ + ("awslambda_function_url_public", "/path/to/awslambda_function_url_public") + ] + + # Mock the return value of load_check_metadata + mock_load_metadata.return_value = mock_metadata_lambda + + bulk_metadata = CheckMetadata.get_bulk(provider="aws") + + result = CheckMetadata.list( + bulk_checks_metadata=bulk_metadata, service="awslambda" + ) + + # Assertions + assert result == {"awslambda_function_url_public"} + @mock.patch("prowler.lib.check.models.load_check_metadata") @mock.patch("prowler.lib.check.models.recover_checks_from_provider") def test_list_by_service_invalid(self, mock_recover_checks, mock_load_metadata): From b69a0d51373572e33b3c416f8e73dae80735aeaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Fern=C3=A1ndez=20Poyatos?= Date: Wed, 27 Nov 2024 15:51:05 +0100 Subject: [PATCH 13/56] feat(overviews): Add API overview endpoints for findings and severity (#5910) --- .github/workflows/api-pull-request.yml | 4 +- .gitignore | 1 + .pre-commit-config.yaml | 3 +- api/poetry.lock | 63 ++- api/src/backend/api/filters.py | 63 ++- .../backend/api/migrations/0001_initial.py | 119 ++++- api/src/backend/api/models.py | 80 +++- api/src/backend/api/specs/v1.yaml | 446 +++++++++++++++++- api/src/backend/api/tests/test_views.py | 25 +- api/src/backend/api/v1/serializers.py | 63 ++- api/src/backend/api/v1/views.py | 257 +++++++--- api/src/backend/tasks/jobs/scan.py | 168 ++++++- api/src/backend/tasks/tasks.py | 13 +- api/src/backend/tasks/tests/test_scan.py | 25 +- 14 files changed, 1132 insertions(+), 198 deletions(-) diff --git a/.github/workflows/api-pull-request.yml b/.github/workflows/api-pull-request.yml index 3da5fed6af..8143ca7f41 100644 --- a/.github/workflows/api-pull-request.yml +++ b/.github/workflows/api-pull-request.yml @@ -1,4 +1,4 @@ -name: "API - Pull Request" +name: "API - Pull Request" on: push: @@ -148,7 +148,7 @@ jobs: working-directory: ./api if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true' run: | - poetry run pytest -n auto --cov=./src/backend --cov-report=xml src/backend + poetry run pytest --cov=./src/backend --cov-report=xml src/backend - name: Upload coverage reports to Codecov if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true' uses: codecov/codecov-action@v5 diff --git a/.gitignore b/.gitignore index f115325102..4c588f5698 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ build/ /dist/ *.egg-info/ */__pycache__/*.pyc +.idea/ # Session Session.vim diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f871bf6de1..2fcb4d4c6f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -97,12 +97,13 @@ repos: - id: safety name: safety description: "Safety is a tool that checks your installed dependencies for known security vulnerabilities" - entry: bash -c 'safety check --ignore 70612' + entry: bash -c 'safety check --ignore 70612,66963' language: system - id: vulture name: vulture description: "Vulture finds unused code in Python programs." entry: bash -c 'vulture --exclude "contrib" --min-confidence 100 .' + exclude: 'api/src/backend/' language: system files: '.*\.py' diff --git a/api/poetry.lock b/api/poetry.lock index ef5e2bfe26..c03e9e4b18 100644 --- a/api/poetry.lock +++ b/api/poetry.lock @@ -439,13 +439,13 @@ azure-core = ">=1.26.2,<2.0.0" [[package]] name = "azure-mgmt-cosmosdb" -version = "9.6.0" +version = "9.7.0" description = "Microsoft Azure Cosmos DB Management Client Library for Python" optional = false python-versions = ">=3.8" files = [ - {file = "azure_mgmt_cosmosdb-9.6.0-py3-none-any.whl", hash = "sha256:02b4108867de58e0b89a206ee7b7588b439e1f6fef2377ce1979b803a0d02d5a"}, - {file = "azure_mgmt_cosmosdb-9.6.0.tar.gz", hash = "sha256:667c7d8a8f542b0e7972e63274af536ad985187e24a6cc2e3c8eef35560881fc"}, + {file = "azure_mgmt_cosmosdb-9.7.0-py3-none-any.whl", hash = "sha256:be735a554d16995c8cefe413e62119985f8fabae1cb45a6f6ad2c3958bed14da"}, + {file = "azure_mgmt_cosmosdb-9.7.0.tar.gz", hash = "sha256:b5072d319f11953d8f12e22459aded1912d5f27e442e1d8b49596a85005410a1"}, ] [package.dependencies] @@ -537,6 +537,22 @@ azure-mgmt-core = ">=1.3.2" isodate = ">=0.6.1" typing-extensions = ">=4.6.0" +[[package]] +name = "azure-mgmt-search" +version = "9.1.0" +description = "Microsoft Azure Search Management Client Library for Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "azure-mgmt-search-9.1.0.tar.gz", hash = "sha256:53bc6eeadb0974d21f120bb21bb5e6827df6d650e17347460fd83e2d68883599"}, + {file = "azure_mgmt_search-9.1.0-py3-none-any.whl", hash = "sha256:488ff81477e980e2b7abf0b857387c74ebbad419e6f6126044e3e6fad2da72b6"}, +] + +[package.dependencies] +azure-common = ">=1.1,<2.0" +azure-mgmt-core = ">=1.3.2,<2.0.0" +isodate = ">=0.6.1,<1.0.0" + [[package]] name = "azure-mgmt-security" version = "7.0.0" @@ -686,17 +702,17 @@ files = [ [[package]] name = "boto3" -version = "1.35.60" +version = "1.35.66" description = "The AWS SDK for Python" optional = false python-versions = ">=3.8" files = [ - {file = "boto3-1.35.60-py3-none-any.whl", hash = "sha256:a34d28de1a1f6ca6ec3edd05c26db16e422293d8f9dcd94f308059a434596753"}, - {file = "boto3-1.35.60.tar.gz", hash = "sha256:e573504c67c3e438fd4b0222119ed1a73b644c78eb3b6dee0b36a6c70ecf7677"}, + {file = "boto3-1.35.66-py3-none-any.whl", hash = "sha256:09a610f8cf4d3c22d4ca69c1f89079e3a1c82805ce94fa0eb4ecdd4d2ba6c4bc"}, + {file = "boto3-1.35.66.tar.gz", hash = "sha256:c392b9168b65e9c23483eaccb5b68d1f960232d7f967a1e00a045ba065ce050d"}, ] [package.dependencies] -botocore = ">=1.35.60,<1.36.0" +botocore = ">=1.35.66,<1.36.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.10.0,<0.11.0" @@ -705,13 +721,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.35.60" +version = "1.35.69" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">=3.8" files = [ - {file = "botocore-1.35.60-py3-none-any.whl", hash = "sha256:ddccfc39a0a55ac0321191a36d29c2ea9be2c96ceefb3928dd3c91c79c494d50"}, - {file = "botocore-1.35.60.tar.gz", hash = "sha256:378f53037d817bed2c04a006b7319745e664030182211429c924647273b29bc9"}, + {file = "botocore-1.35.69-py3-none-any.whl", hash = "sha256:cad8d9305f873404eee4b197d84e60a40975d43cbe1ab63abe893420ddfe6e3c"}, + {file = "botocore-1.35.69.tar.gz", hash = "sha256:f9f23dd76fb247d9b0e8d411d2995e6f847fc451c026f1e58e300f815b0b36eb"}, ] [package.dependencies] @@ -1891,13 +1907,13 @@ grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] [[package]] name = "google-api-python-client" -version = "2.153.0" +version = "2.154.0" description = "Google API Client Library for Python" optional = false python-versions = ">=3.7" files = [ - {file = "google_api_python_client-2.153.0-py2.py3-none-any.whl", hash = "sha256:6ff13bbfa92a57972e33ec3808e18309e5981b8ca1300e5da23bf2b4d6947384"}, - {file = "google_api_python_client-2.153.0.tar.gz", hash = "sha256:35cce8647f9c163fc04fb4d811fc91aae51954a2bdd74918decbe0e65d791dd2"}, + {file = "google_api_python_client-2.154.0-py2.py3-none-any.whl", hash = "sha256:a521bbbb2ec0ba9d6f307cdd64ed6e21eeac372d1bd7493a4ab5022941f784ad"}, + {file = "google_api_python_client-2.154.0.tar.gz", hash = "sha256:1b420062e03bfcaa1c79e2e00a612d29a6a934151ceb3d272fe150a656dc8f17"}, ] [package.dependencies] @@ -3275,7 +3291,7 @@ files = [ [[package]] name = "prowler" -version = "4.6.0" +version = "5.0.0" description = "Prowler is an Open Source security tool to perform AWS, GCP and Azure security best practices assessments, audits, incident response, continuous monitoring, hardening and forensics readiness. It contains hundreds of controls covering CIS, NIST 800, NIST CSF, CISA, RBI, FedRAMP, PCI-DSS, GDPR, HIPAA, FFIEC, SOC2, GXP, AWS Well-Architected Framework Security Pillar, AWS Foundational Technical Review (FTR), ENS (Spanish National Security Scheme) and your custom security frameworks." optional = false python-versions = ">=3.9,<3.13" @@ -3292,26 +3308,27 @@ azure-mgmt-authorization = "4.0.0" azure-mgmt-compute = "33.0.0" azure-mgmt-containerregistry = "10.3.0" azure-mgmt-containerservice = "33.0.0" -azure-mgmt-cosmosdb = "9.6.0" +azure-mgmt-cosmosdb = "9.7.0" azure-mgmt-keyvault = "10.3.1" azure-mgmt-monitor = "6.0.2" azure-mgmt-network = "28.0.0" azure-mgmt-rdbms = "10.1.0" azure-mgmt-resource = "23.2.0" +azure-mgmt-search = "9.1.0" azure-mgmt-security = "7.0.0" azure-mgmt-sql = "3.0.1" azure-mgmt-storage = "21.2.1" azure-mgmt-subscription = "3.1.1" azure-mgmt-web = "7.3.1" azure-storage-blob = "12.24.0" -boto3 = "1.35.60" -botocore = "1.35.60" +boto3 = "1.35.66" +botocore = "1.35.69" colorama = "0.4.6" cryptography = "43.0.1" dash = "2.18.2" dash-bootstrap-components = "1.6.0" detect-secrets = "1.5.0" -google-api-python-client = "2.153.0" +google-api-python-client = "2.154.0" google-auth-httplib2 = ">=0.1,<0.3" jsonschema = "4.23.0" kubernetes = "31.0.0" @@ -3325,7 +3342,7 @@ python-dateutil = "^2.9.0.post0" pytz = "2024.2" schema = "0.7.7" shodan = "1.31.0" -slack-sdk = "3.33.3" +slack-sdk = "3.33.4" tabulate = "0.9.0" tzlocal = "5.2" @@ -3333,7 +3350,7 @@ tzlocal = "5.2" type = "git" url = "https://github.com/prowler-cloud/prowler.git" reference = "master" -resolved_reference = "8be83fc632445cd25eeb90ed20257716b673cead" +resolved_reference = "9c383baff309d868b37934b694df9aacba397fad" [[package]] name = "psutil" @@ -4458,13 +4475,13 @@ files = [ [[package]] name = "slack-sdk" -version = "3.33.3" +version = "3.33.4" description = "The Slack API Platform SDK for Python" optional = false python-versions = ">=3.6" files = [ - {file = "slack_sdk-3.33.3-py2.py3-none-any.whl", hash = "sha256:0515fb93cd03b18de61f876a8304c4c3cef4dd3c2a3bad62d7394d2eb5a3c8e6"}, - {file = "slack_sdk-3.33.3.tar.gz", hash = "sha256:4cc44c9ffe4bb28a01fbe3264c2f466c783b893a4eca62026ab845ec7c176ff1"}, + {file = "slack_sdk-3.33.4-py2.py3-none-any.whl", hash = "sha256:9f30cb3c9c07b441c49d53fc27f9f1837ad1592a7e9d4ca431f53cdad8826cc6"}, + {file = "slack_sdk-3.33.4.tar.gz", hash = "sha256:5e109847f6b6a22d227609226ba4ed936109dc00675bddeb7e0bee502d3ee7e0"}, ] [package.extras] diff --git a/api/src/backend/api/filters.py b/api/src/backend/api/filters.py index 2ed175c5d7..7a3e850430 100644 --- a/api/src/backend/api/filters.py +++ b/api/src/backend/api/filters.py @@ -4,47 +4,48 @@ from django.conf import settings from django.db.models import Q from django_filters.rest_framework import ( BaseInFilter, - FilterSet, BooleanFilter, CharFilter, - UUIDFilter, - DateFilter, ChoiceFilter, + DateFilter, + FilterSet, + UUIDFilter, ) from rest_framework_json_api.django_filters.backends import DjangoFilterBackend from rest_framework_json_api.serializers import ValidationError from api.db_utils import ( - ProviderEnumField, FindingDeltaEnumField, - StatusEnumField, - SeverityEnumField, InvitationStateEnumField, + ProviderEnumField, + SeverityEnumField, + StatusEnumField, ) from api.models import ( - User, + ComplianceOverview, + Finding, + Invitation, Membership, Provider, ProviderGroup, + ProviderSecret, Resource, ResourceTag, Scan, - Task, - StateChoices, - Finding, + ScanSummary, SeverityChoices, + StateChoices, StatusChoices, - ProviderSecret, - Invitation, - ComplianceOverview, + Task, + User, ) from api.rls import Tenant from api.uuid_utils import ( datetime_to_uuid7, - uuid7_start, + transform_into_uuid7, uuid7_end, uuid7_range, - transform_into_uuid7, + uuid7_start, ) from api.v1.serializers import TaskBase @@ -57,6 +58,13 @@ class CustomDjangoFilterBackend(DjangoFilterBackend): """ return None + def get_filterset_class(self, view, queryset=None): + # Check if the view has 'get_filterset_class' method + if hasattr(view, "get_filterset_class"): + return view.get_filterset_class() + # Fallback to the default implementation + return super().get_filterset_class(view, queryset) + class UUIDInFilter(BaseInFilter, UUIDFilter): pass @@ -482,3 +490,28 @@ class ComplianceOverviewFilter(FilterSet): "version": ["exact", "icontains"], "region": ["exact", "icontains", "in"], } + + +class ScanSummaryFilter(FilterSet): + inserted_at = DateFilter(field_name="inserted_at", lookup_expr="date") + provider_id = UUIDFilter(field_name="scan__provider__id", lookup_expr="exact") + provider_type = ChoiceFilter( + field_name="scan__provider__provider", choices=Provider.ProviderChoices.choices + ) + provider_type__in = ChoiceInFilter( + field_name="scan__provider__provider", choices=Provider.ProviderChoices.choices + ) + region = CharFilter(field_name="region") + muted_findings = BooleanFilter(method="filter_muted_findings") + + def filter_muted_findings(self, queryset, name, value): + if not value: + return queryset.exclude(muted__gt=0) + return queryset + + class Meta: + model = ScanSummary + fields = { + "inserted_at": ["date", "gte", "lte"], + "region": ["exact", "icontains", "in"], + } diff --git a/api/src/backend/api/migrations/0001_initial.py b/api/src/backend/api/migrations/0001_initial.py index d2a8a39d3b..dc492f17c5 100644 --- a/api/src/backend/api/migrations/0001_initial.py +++ b/api/src/backend/api/migrations/0001_initial.py @@ -22,36 +22,36 @@ from uuid6 import uuid7 import api.rls from api.db_utils import ( - PostgresEnumMigration, - MemberRoleEnumField, + DB_PROWLER_PASSWORD, + DB_PROWLER_USER, + POSTGRES_TENANT_VAR, + POSTGRES_USER_VAR, + TASK_RUNNER_DB_TABLE, + InvitationStateEnum, + InvitationStateEnumField, MemberRoleEnum, + MemberRoleEnumField, + PostgresEnumMigration, ProviderEnum, ProviderEnumField, ProviderSecretTypeEnum, ProviderSecretTypeEnumField, ScanTriggerEnum, - StateEnumField, - StateEnum, ScanTriggerEnumField, - InvitationStateEnum, - InvitationStateEnumField, + StateEnum, + StateEnumField, register_enum, - DB_PROWLER_USER, - DB_PROWLER_PASSWORD, - TASK_RUNNER_DB_TABLE, - POSTGRES_TENANT_VAR, - POSTGRES_USER_VAR, ) from api.models import ( - Provider, - Scan, - StateChoices, Finding, - StatusChoices, - SeverityChoices, - Membership, - ProviderSecret, Invitation, + Membership, + Provider, + ProviderSecret, + Scan, + SeverityChoices, + StateChoices, + StatusChoices, ) DB_NAME = settings.DATABASES["default"]["NAME"] @@ -289,7 +289,8 @@ class Migration(migrations.Migration): ), ), # Enable tenants RLS based on memberships - migrations.RunSQL(f""" + migrations.RunSQL( + f""" ALTER TABLE tenants ENABLE ROW LEVEL SECURITY; -- Policy for SELECT @@ -364,7 +365,8 @@ class Migration(migrations.Migration): FOR INSERT TO {DB_PROWLER_USER} WITH CHECK (true); - """), + """ + ), # Create and register ProviderEnum type migrations.RunPython( ProviderEnumMigration.create_enum_type, @@ -1482,4 +1484,81 @@ class Migration(migrations.Migration): name="comp_ov_cp_id_req_fail_idx", ), ), + migrations.CreateModel( + name="ScanSummary", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ("inserted_at", models.DateTimeField(auto_now_add=True)), + ("check_id", models.CharField(max_length=100)), + ("service", models.TextField()), + ( + "severity", + api.db_utils.SeverityEnumField( + choices=[ + ("critical", "Critical"), + ("high", "High"), + ("medium", "Medium"), + ("low", "Low"), + ("informational", "Informational"), + ] + ), + ), + ("region", models.TextField()), + ("_pass", models.IntegerField(db_column="pass", default=0)), + ("fail", models.IntegerField(default=0)), + ("muted", models.IntegerField(default=0)), + ("total", models.IntegerField(default=0)), + ("new", models.IntegerField(default=0)), + ("changed", models.IntegerField(default=0)), + ("unchanged", models.IntegerField(default=0)), + ("fail_new", models.IntegerField(default=0)), + ("fail_changed", models.IntegerField(default=0)), + ("pass_new", models.IntegerField(default=0)), + ("pass_changed", models.IntegerField(default=0)), + ("muted_new", models.IntegerField(default=0)), + ("muted_changed", models.IntegerField(default=0)), + ( + "scan", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="aggregations", + related_query_name="aggregation", + to="api.scan", + ), + ), + ( + "tenant", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="api.tenant" + ), + ), + ], + options={ + "db_table": "scan_summaries", + "abstract": False, + }, + ), + migrations.AddConstraint( + model_name="scansummary", + constraint=api.rls.RowLevelSecurityConstraint( + "tenant_id", + name="rls_on_scansummary", + statements=["SELECT", "INSERT", "UPDATE", "DELETE"], + ), + ), + migrations.AddConstraint( + model_name="scansummary", + constraint=models.UniqueConstraint( + fields=("tenant", "scan", "check_id", "service", "severity", "region"), + name="unique_scan_summary", + ), + ), ] diff --git a/api/src/backend/api/models.py b/api/src/backend/api/models.py index f498988f88..664bc818fb 100644 --- a/api/src/backend/api/models.py +++ b/api/src/backend/api/models.py @@ -1,6 +1,6 @@ import json import re -from uuid import uuid4, UUID +from uuid import UUID, uuid4 from cryptography.fernet import Fernet from django.conf import settings @@ -11,35 +11,33 @@ from django.core.validators import MinLengthValidator from django.db import models from django.utils.translation import gettext_lazy as _ from django_celery_results.models import TaskResult -from prowler.lib.check.models import Severity from psqlextra.models import PostgresPartitionedModel from psqlextra.types import PostgresPartitioningMethod from uuid6 import uuid7 from api.db_utils import ( - MemberRoleEnumField, - enum_to_choices, - ProviderEnumField, - StateEnumField, - ScanTriggerEnumField, - FindingDeltaEnumField, - SeverityEnumField, - StatusEnumField, CustomUserManager, - ProviderSecretTypeEnumField, + FindingDeltaEnumField, InvitationStateEnumField, - one_week_from_now, + MemberRoleEnumField, + ProviderEnumField, + ProviderSecretTypeEnumField, + ScanTriggerEnumField, + SeverityEnumField, + StateEnumField, + StatusEnumField, + enum_to_choices, generate_random_token, + one_week_from_now, ) from api.exceptions import ModelValidationError from api.rls import ( - RowLevelSecurityProtectedModel, -) -from api.rls import ( - Tenant, - RowLevelSecurityConstraint, BaseSecurityConstraint, + RowLevelSecurityConstraint, + RowLevelSecurityProtectedModel, + Tenant, ) +from prowler.lib.check.models import Severity fernet = Fernet(settings.SECRETS_ENCRYPTION_KEY.encode()) @@ -856,3 +854,51 @@ class ComplianceOverview(RowLevelSecurityProtectedModel): class JSONAPIMeta: resource_name = "compliance-overviews" + + +class ScanSummary(RowLevelSecurityProtectedModel): + id = models.UUIDField(primary_key=True, default=uuid4, editable=False) + inserted_at = models.DateTimeField(auto_now_add=True, editable=False) + check_id = models.CharField(max_length=100, blank=False, null=False) + service = models.TextField(blank=False) + severity = SeverityEnumField(choices=SeverityChoices) + region = models.TextField(blank=False) + _pass = models.IntegerField(db_column="pass", default=0) + fail = models.IntegerField(default=0) + muted = models.IntegerField(default=0) + total = models.IntegerField(default=0) + new = models.IntegerField(default=0) + changed = models.IntegerField(default=0) + unchanged = models.IntegerField(default=0) + + fail_new = models.IntegerField(default=0) + fail_changed = models.IntegerField(default=0) + pass_new = models.IntegerField(default=0) + pass_changed = models.IntegerField(default=0) + muted_new = models.IntegerField(default=0) + muted_changed = models.IntegerField(default=0) + + scan = models.ForeignKey( + Scan, + on_delete=models.CASCADE, + related_name="aggregations", + related_query_name="aggregation", + ) + + class Meta(RowLevelSecurityProtectedModel.Meta): + db_table = "scan_summaries" + + constraints = [ + models.UniqueConstraint( + fields=("tenant", "scan", "check_id", "service", "severity", "region"), + name="unique_scan_summary", + ), + RowLevelSecurityConstraint( + field="tenant_id", + name="rls_on_%(class)s", + statements=["SELECT", "INSERT", "UPDATE", "DELETE"], + ), + ] + + class JSONAPIMeta: + resource_name = "scan-summaries" diff --git a/api/src/backend/api/specs/v1.yaml b/api/src/backend/api/specs/v1.yaml index 755ab8c8df..fdc83418a3 100644 --- a/api/src/backend/api/specs/v1.yaml +++ b/api/src/backend/api/specs/v1.yaml @@ -780,16 +780,332 @@ paths: schema: $ref: '#/components/schemas/OpenApiResponseResponse' description: '' + /api/v1/overviews/findings: + get: + operationId: overviews_findings_retrieve + description: Fetch aggregated findings data across all providers, grouped by + various metrics such as passed, failed, muted, and total findings. This endpoint + calculates summary statistics based on the latest scans for each provider + and applies any provided filters, such as region, provider type, and scan + date. + summary: Get aggregated findings data + parameters: + - in: query + name: fields[findings-overview] + schema: + type: array + items: + type: string + enum: + - id + - new + - changed + - unchanged + - fail_new + - fail_changed + - pass_new + - pass_changed + - muted_new + - muted_changed + - total + - fail + - muted + - pass + description: endpoint return only specific fields in the response on a per-type + basis by including a fields[TYPE] query parameter. + explode: false + - in: query + name: filter[inserted_at] + schema: + type: string + format: date + - in: query + name: filter[inserted_at__date] + schema: + type: string + format: date + - in: query + name: filter[inserted_at__gte] + schema: + type: string + format: date-time + - in: query + name: filter[inserted_at__lte] + schema: + type: string + format: date-time + - in: query + name: filter[muted_findings] + schema: + type: boolean + - in: query + name: filter[provider_id] + schema: + type: string + format: uuid + - in: query + name: filter[provider_type] + schema: + type: string + enum: + - aws + - azure + - gcp + - kubernetes + description: |- + * `aws` - AWS + * `azure` - Azure + * `gcp` - GCP + * `kubernetes` - Kubernetes + - in: query + name: filter[provider_type__in] + schema: + type: array + items: + type: string + enum: + - aws + - azure + - gcp + - kubernetes + description: |- + Multiple values may be separated by commas. + + * `aws` - AWS + * `azure` - Azure + * `gcp` - GCP + * `kubernetes` - Kubernetes + explode: false + style: form + - in: query + name: filter[region] + schema: + type: string + - in: query + name: filter[region__icontains] + schema: + type: string + - in: query + name: filter[region__in] + schema: + type: array + items: + type: string + description: Multiple values may be separated by commas. + explode: false + style: form + - name: filter[search] + required: false + in: query + description: A search term. + schema: + type: string + - name: sort + required: false + in: query + description: '[list of fields to sort by](https://jsonapi.org/format/#fetching-sorting)' + schema: + type: array + items: + type: string + enum: + - id + - -id + - new + - -new + - changed + - -changed + - unchanged + - -unchanged + - fail_new + - -fail_new + - fail_changed + - -fail_changed + - pass_new + - -pass_new + - pass_changed + - -pass_changed + - muted_new + - -muted_new + - muted_changed + - -muted_changed + - total + - -total + - fail + - -fail + - muted + - -muted + - pass + - -pass + explode: false + tags: + - Overview + security: + - jwtAuth: [] + responses: + '200': + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/OverviewFindingResponse' + description: '' + /api/v1/overviews/findings_severity: + get: + operationId: overviews_findings_severity_retrieve + description: Retrieve an aggregated summary of findings grouped by severity + levels, such as low, medium, high, and critical. The response includes the + total count of findings for each severity, considering only the latest scans + for each provider. Additional filters can be applied to narrow down results + by region, provider type, or other attributes. + summary: Get findings data by severity + parameters: + - in: query + name: fields[findings-severity-overview] + schema: + type: array + items: + type: string + enum: + - id + - critical + - high + - medium + - low + - informational + description: endpoint return only specific fields in the response on a per-type + basis by including a fields[TYPE] query parameter. + explode: false + - in: query + name: filter[inserted_at] + schema: + type: string + format: date + - in: query + name: filter[inserted_at__date] + schema: + type: string + format: date + - in: query + name: filter[inserted_at__gte] + schema: + type: string + format: date-time + - in: query + name: filter[inserted_at__lte] + schema: + type: string + format: date-time + - in: query + name: filter[muted_findings] + schema: + type: boolean + - in: query + name: filter[provider_id] + schema: + type: string + format: uuid + - in: query + name: filter[provider_type] + schema: + type: string + enum: + - aws + - azure + - gcp + - kubernetes + description: |- + * `aws` - AWS + * `azure` - Azure + * `gcp` - GCP + * `kubernetes` - Kubernetes + - in: query + name: filter[provider_type__in] + schema: + type: array + items: + type: string + enum: + - aws + - azure + - gcp + - kubernetes + description: |- + Multiple values may be separated by commas. + + * `aws` - AWS + * `azure` - Azure + * `gcp` - GCP + * `kubernetes` - Kubernetes + explode: false + style: form + - in: query + name: filter[region] + schema: + type: string + - in: query + name: filter[region__icontains] + schema: + type: string + - in: query + name: filter[region__in] + schema: + type: array + items: + type: string + description: Multiple values may be separated by commas. + explode: false + style: form + - name: filter[search] + required: false + in: query + description: A search term. + schema: + type: string + - name: sort + required: false + in: query + description: '[list of fields to sort by](https://jsonapi.org/format/#fetching-sorting)' + schema: + type: array + items: + type: string + enum: + - id + - -id + - critical + - -critical + - high + - -high + - medium + - -medium + - low + - -low + - informational + - -informational + explode: false + tags: + - Overview + security: + - jwtAuth: [] + responses: + '200': + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/OverviewSeverityResponse' + description: '' /api/v1/overviews/providers: get: operationId: overviews_providers_retrieve - description: Fetch aggregated summaries of the latest findings and resources - for each provider. This includes counts of passed, failed, and manual findings, - as well as the total number of resources managed by each provider. - summary: List aggregated overview data for providers + description: Retrieve an aggregated overview of findings and resources grouped + by providers. The response includes the count of passed, failed, and manual + findings, along with the total number of resources managed by each provider. + Only the latest findings for each provider are considered in the aggregation + to ensure accurate and up-to-date insights. + summary: Get aggregated provider data parameters: - in: query - name: fields[provider-overviews] + name: fields[providers-overview] schema: type: array items: @@ -4400,6 +4716,77 @@ components: $ref: '#/components/schemas/Membership' required: - data + OverviewFinding: + type: object + required: + - type + - id + additionalProperties: false + properties: + type: + allOf: + - $ref: '#/components/schemas/OverviewFindingTypeEnum' + description: The [type](https://jsonapi.org/format/#document-resource-object-identification) + member is used to describe resource objects that share common attributes + and relationships. + id: {} + attributes: + type: object + properties: + id: + type: string + default: n/a + new: + type: integer + changed: + type: integer + unchanged: + type: integer + fail_new: + type: integer + fail_changed: + type: integer + pass_new: + type: integer + pass_changed: + type: integer + muted_new: + type: integer + muted_changed: + type: integer + total: + type: integer + fail: + type: integer + muted: + type: integer + pass: + type: integer + required: + - new + - changed + - unchanged + - fail_new + - fail_changed + - pass_new + - pass_changed + - muted_new + - muted_changed + - total + - fail + - muted + - pass + OverviewFindingResponse: + type: object + properties: + data: + $ref: '#/components/schemas/OverviewFinding' + required: + - data + OverviewFindingTypeEnum: + type: string + enum: + - findings-overview OverviewProvider: type: object required: @@ -4449,7 +4836,54 @@ components: OverviewProviderTypeEnum: type: string enum: - - provider-overviews + - providers-overview + OverviewSeverity: + type: object + required: + - type + - id + additionalProperties: false + properties: + type: + allOf: + - $ref: '#/components/schemas/OverviewSeverityTypeEnum' + description: The [type](https://jsonapi.org/format/#document-resource-object-identification) + member is used to describe resource objects that share common attributes + and relationships. + id: {} + attributes: + type: object + properties: + id: + type: string + default: n/a + critical: + type: integer + high: + type: integer + medium: + type: integer + low: + type: integer + informational: + type: integer + required: + - critical + - high + - medium + - low + - informational + OverviewSeverityResponse: + type: object + properties: + data: + $ref: '#/components/schemas/OverviewSeverity' + required: + - data + OverviewSeverityTypeEnum: + type: string + enum: + - findings-severity-overview PaginatedComplianceOverviewList: type: object required: diff --git a/api/src/backend/api/tests/test_views.py b/api/src/backend/api/tests/test_views.py index 79589968bd..1178f6c276 100644 --- a/api/src/backend/api/tests/test_views.py +++ b/api/src/backend/api/tests/test_views.py @@ -1,29 +1,24 @@ import json -from datetime import datetime -from datetime import timezone, timedelta +from datetime import datetime, timedelta, timezone from unittest.mock import ANY, Mock, patch import jwt import pytest +from conftest import API_JSON_CONTENT_TYPE, TEST_PASSWORD, TEST_USER from django.urls import reverse from rest_framework import status from api.models import ( - User, + Invitation, Membership, Provider, ProviderGroup, ProviderGroupMembership, - Scan, ProviderSecret, - Invitation, + Scan, + User, ) from api.rls import Tenant -from conftest import ( - API_JSON_CONTENT_TYPE, - TEST_PASSWORD, - TEST_USER, -) TODAY = str(datetime.today().date()) @@ -1794,7 +1789,7 @@ class TestScanViewSet: ], ) @patch("api.v1.views.Task.objects.get") - @patch("api.v1.views.perform_scan_task.delay") + @patch("api.v1.views.perform_scan_task.apply_async") def test_scans_create_valid( self, mock_perform_scan_task, @@ -3009,9 +3004,9 @@ class TestInvitationViewSet: response = authenticated_client.get( reverse("invitation-list"), { - f"filter[{filter_name}]": filter_value - if filter_name != "inviter" - else str(user.id) + f"filter[{filter_name}]": ( + filter_value if filter_name != "inviter" else str(user.id) + ) }, ) @@ -3262,3 +3257,5 @@ class TestOverviewViewSet: assert response.json()["data"][0]["attributes"]["resources"]["total"] == len( resources_fixture ) + + # TODO Add more tests for the rest of overviews diff --git a/api/src/backend/api/v1/serializers.py b/api/src/backend/api/v1/serializers.py index 9b0895adcb..9cc28bf009 100644 --- a/api/src/backend/api/v1/serializers.py +++ b/api/src/backend/api/v1/serializers.py @@ -1,5 +1,5 @@ import json -from datetime import datetime, timezone, timedelta +from datetime import datetime, timedelta, timezone from django.conf import settings from django.contrib.auth import authenticate @@ -14,24 +14,23 @@ from rest_framework_simplejwt.serializers import TokenObtainPairSerializer from rest_framework_simplejwt.tokens import RefreshToken from api.models import ( - StateChoices, - User, + ComplianceOverview, + Finding, + Invitation, Membership, Provider, ProviderGroup, ProviderGroupMembership, - Scan, - Task, + ProviderSecret, Resource, ResourceTag, - Finding, - ProviderSecret, - Invitation, - ComplianceOverview, + Scan, + StateChoices, + Task, + User, ) from api.rls import Tenant - # Tokens @@ -1234,7 +1233,7 @@ class OverviewProviderSerializer(serializers.Serializer): resources = serializers.SerializerMethodField(read_only=True) class JSONAPIMeta: - resource_name = "provider-overviews" + resource_name = "providers-overview" def get_root_meta(self, _resource, _many): return {"version": "v1"} @@ -1270,3 +1269,45 @@ class OverviewProviderSerializer(serializers.Serializer): return { "total": obj["total_resources"], } + + +class OverviewFindingSerializer(serializers.Serializer): + id = serializers.CharField(default="n/a") + new = serializers.IntegerField() + changed = serializers.IntegerField() + unchanged = serializers.IntegerField() + fail_new = serializers.IntegerField() + fail_changed = serializers.IntegerField() + pass_new = serializers.IntegerField() + pass_changed = serializers.IntegerField() + muted_new = serializers.IntegerField() + muted_changed = serializers.IntegerField() + total = serializers.IntegerField() + _pass = serializers.IntegerField() + fail = serializers.IntegerField() + muted = serializers.IntegerField() + + class JSONAPIMeta: + resource_name = "findings-overview" + + def get_root_meta(self, _resource, _many): + return {"version": "v1"} + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields["pass"] = self.fields.pop("_pass") + + +class OverviewSeveritySerializer(serializers.Serializer): + id = serializers.CharField(default="n/a") + critical = serializers.IntegerField() + high = serializers.IntegerField() + medium = serializers.IntegerField() + low = serializers.IntegerField() + informational = serializers.IntegerField() + + class JSONAPIMeta: + resource_name = "findings-severity-overview" + + def get_root_meta(self, _resource, _many): + return {"version": "v1"} diff --git a/api/src/backend/api/v1/views.py b/api/src/backend/api/v1/views.py index 70af1069ab..90db12150a 100644 --- a/api/src/backend/api/v1/views.py +++ b/api/src/backend/api/v1/views.py @@ -2,20 +2,20 @@ from celery.result import AsyncResult from django.conf import settings as django_settings from django.contrib.postgres.search import SearchQuery from django.db import transaction -from django.db.models import Prefetch, Subquery, OuterRef, Count, Q, F +from django.db.models import Count, F, OuterRef, Prefetch, Q, Subquery, Sum from django.urls import reverse from django.utils.decorators import method_decorator from django.views.decorators.cache import cache_control from drf_spectacular.settings import spectacular_settings from drf_spectacular.utils import ( - extend_schema, - extend_schema_view, OpenApiParameter, OpenApiResponse, OpenApiTypes, + extend_schema, + extend_schema_view, ) from drf_spectacular.views import SpectacularAPIView -from rest_framework import status, permissions +from rest_framework import permissions, status from rest_framework.decorators import action from rest_framework.exceptions import ( MethodNotAllowed, @@ -23,82 +23,87 @@ from rest_framework.exceptions import ( PermissionDenied, ValidationError, ) -from rest_framework.generics import get_object_or_404, GenericAPIView +from rest_framework.generics import GenericAPIView, get_object_or_404 from rest_framework_json_api.views import Response -from rest_framework_simplejwt.exceptions import InvalidToken -from rest_framework_simplejwt.exceptions import TokenError +from rest_framework_simplejwt.exceptions import InvalidToken, TokenError +from tasks.beat import schedule_provider_scan +from tasks.tasks import ( + check_provider_connection_task, + delete_provider_task, + perform_scan_summary_task, + perform_scan_task, +) -from api.base_views import BaseTenantViewset, BaseRLSViewSet, BaseUserViewset +from api.base_views import BaseRLSViewSet, BaseTenantViewset, BaseUserViewset from api.db_router import MainRouter from api.filters import ( + ComplianceOverviewFilter, + FindingFilter, + InvitationFilter, + MembershipFilter, ProviderFilter, ProviderGroupFilter, - TenantFilter, - MembershipFilter, - ScanFilter, - TaskFilter, - ResourceFilter, - FindingFilter, ProviderSecretFilter, - InvitationFilter, + ResourceFilter, + ScanFilter, + ScanSummaryFilter, + TaskFilter, + TenantFilter, UserFilter, - ComplianceOverviewFilter, ) from api.models import ( - StatusChoices, - User, + ComplianceOverview, + Finding, + Invitation, Membership, Provider, ProviderGroup, ProviderGroupMembership, - Scan, - Task, - Resource, - Finding, ProviderSecret, - Invitation, - ComplianceOverview, + Resource, + Scan, + ScanSummary, + SeverityChoices, + StatusChoices, + Task, + User, ) from api.pagination import ComplianceOverviewPagination from api.rls import Tenant from api.utils import validate_invitation from api.uuid_utils import datetime_to_uuid7 from api.v1.serializers import ( - TokenSerializer, - TokenRefreshSerializer, - UserSerializer, - UserCreateSerializer, - UserUpdateSerializer, + ComplianceOverviewFullSerializer, + ComplianceOverviewSerializer, + FindingSerializer, + InvitationAcceptSerializer, + InvitationCreateSerializer, + InvitationSerializer, + InvitationUpdateSerializer, MembershipSerializer, + OverviewFindingSerializer, + OverviewProviderSerializer, + OverviewSeveritySerializer, + ProviderCreateSerializer, + ProviderGroupMembershipUpdateSerializer, ProviderGroupSerializer, ProviderGroupUpdateSerializer, - ProviderGroupMembershipUpdateSerializer, - ProviderSerializer, - ProviderCreateSerializer, - ProviderUpdateSerializer, - TenantSerializer, - TaskSerializer, - ScanSerializer, - ScanCreateSerializer, - ScanUpdateSerializer, - ResourceSerializer, - FindingSerializer, + ProviderSecretCreateSerializer, ProviderSecretSerializer, ProviderSecretUpdateSerializer, - ProviderSecretCreateSerializer, - InvitationSerializer, - InvitationCreateSerializer, - InvitationUpdateSerializer, - InvitationAcceptSerializer, - ComplianceOverviewSerializer, - ComplianceOverviewFullSerializer, - OverviewProviderSerializer, -) -from tasks.beat import schedule_provider_scan -from tasks.tasks import ( - check_provider_connection_task, - delete_provider_task, - perform_scan_task, + ProviderSerializer, + ProviderUpdateSerializer, + ResourceSerializer, + ScanCreateSerializer, + ScanSerializer, + ScanUpdateSerializer, + TaskSerializer, + TenantSerializer, + TokenRefreshSerializer, + TokenSerializer, + UserCreateSerializer, + UserSerializer, + UserUpdateSerializer, ) CACHE_DECORATOR = cache_control( @@ -803,12 +808,18 @@ class ScanViewSet(BaseRLSViewSet): with transaction.atomic(): scan = input_serializer.save() with transaction.atomic(): - task = perform_scan_task.delay( - tenant_id=request.tenant_id, - scan_id=str(scan.id), - provider_id=str(scan.provider_id), - # Disabled for now - # checks_to_execute=scan.scanner_args.get("checks_to_execute"), + task = perform_scan_task.apply_async( + kwargs={ + "tenant_id": request.tenant_id, + "scan_id": str(scan.id), + "provider_id": str(scan.provider_id), + # Disabled for now + # checks_to_execute=scan.scanner_args.get("checks_to_execute"), + }, + link=perform_scan_summary_task.si( + tenant_id=request.tenant_id, + scan_id=str(scan.id), + ), ) scan.task_id = task.id @@ -1295,26 +1306,67 @@ class ComplianceOverviewViewSet(BaseRLSViewSet): @extend_schema(tags=["Overview"]) @extend_schema_view( providers=extend_schema( - summary="List aggregated overview data for providers", - description="Fetch aggregated summaries of the latest findings and resources for each provider. " - "This includes counts of passed, failed, and manual findings, as well as the total number " - "of resources managed by each provider.", + summary="Get aggregated provider data", + description=( + "Retrieve an aggregated overview of findings and resources grouped by providers. " + "The response includes the count of passed, failed, and manual findings, along with " + "the total number of resources managed by each provider. Only the latest findings for " + "each provider are considered in the aggregation to ensure accurate and up-to-date insights." + ), + ), + findings=extend_schema( + summary="Get aggregated findings data", + description=( + "Fetch aggregated findings data across all providers, grouped by various metrics such as " + "passed, failed, muted, and total findings. This endpoint calculates summary statistics " + "based on the latest scans for each provider and applies any provided filters, such as " + "region, provider type, and scan date." + ), + filters=True, + ), + findings_severity=extend_schema( + summary="Get findings data by severity", + description=( + "Retrieve an aggregated summary of findings grouped by severity levels, such as low, medium, " + "high, and critical. The response includes the total count of findings for each severity, " + "considering only the latest scans for each provider. Additional filters can be applied to " + "narrow down results by region, provider type, or other attributes." + ), + filters=True, ), ) @method_decorator(CACHE_DECORATOR, name="list") class OverviewViewSet(BaseRLSViewSet): queryset = ComplianceOverview.objects.all() http_method_names = ["get"] - ordering = ["compliance_id"] + ordering = ["-id"] def get_queryset(self): - return Finding.objects.all() + if self.action == "providers": + return Finding.objects.all() + elif self.action == "findings": + return ScanSummary.objects.all() + elif self.action == "findings_severity": + return ScanSummary.objects.all() + else: + return super().get_queryset() def get_serializer_class(self): if self.action == "providers": return OverviewProviderSerializer + elif self.action == "findings": + return OverviewFindingSerializer + elif self.action == "findings_severity": + return OverviewSeveritySerializer return super().get_serializer_class() + def get_filterset_class(self): + if self.action == "providers": + return None + elif self.action in ["findings", "findings_severity"]: + return ScanSummaryFilter + return None + @extend_schema(exclude=True) def list(self, request, *args, **kwargs): raise MethodNotAllowed(method="GET") @@ -1366,7 +1418,7 @@ class OverviewViewSet(BaseRLSViewSet): for res in resources_aggregated if res["provider__provider"] == provider ), - 0, # Default to 0 if no resources are found + 0, ) overview.append( { @@ -1382,3 +1434,74 @@ class OverviewViewSet(BaseRLSViewSet): serializer = OverviewProviderSerializer(overview, many=True) return Response(serializer.data, status=status.HTTP_200_OK) + + @action(detail=False, methods=["get"], url_name="findings") + def findings(self, request): + queryset = self.get_queryset() + filtered_queryset = self.filter_queryset(queryset) + + latest_scan_subquery = ( + Scan.objects.filter(provider_id=OuterRef("scan__provider_id")) + .order_by("-id") + .values("id")[:1] + ) + + annotated_queryset = filtered_queryset.annotate( + latest_scan_id=Subquery(latest_scan_subquery) + ) + + filtered_queryset = annotated_queryset.filter(scan_id=F("latest_scan_id")) + + aggregated_totals = filtered_queryset.aggregate( + _pass=Sum("_pass") or 0, + fail=Sum("fail") or 0, + muted=Sum("muted") or 0, + total=Sum("total") or 0, + new=Sum("new") or 0, + changed=Sum("changed") or 0, + unchanged=Sum("unchanged") or 0, + fail_new=Sum("fail_new") or 0, + fail_changed=Sum("fail_changed") or 0, + pass_new=Sum("pass_new") or 0, + pass_changed=Sum("pass_changed") or 0, + muted_new=Sum("muted_new") or 0, + muted_changed=Sum("muted_changed") or 0, + ) + + for key in aggregated_totals: + if aggregated_totals[key] is None: + aggregated_totals[key] = 0 + + serializer = self.get_serializer(aggregated_totals) + return Response(serializer.data, status=status.HTTP_200_OK) + + @action(detail=False, methods=["get"], url_name="findings_severity") + def findings_severity(self, request): + queryset = self.get_queryset() + filtered_queryset = self.filter_queryset(queryset) + + latest_scan_subquery = ( + Scan.objects.filter(provider_id=OuterRef("scan__provider_id")) + .order_by("-id") + .values("id")[:1] + ) + + annotated_queryset = filtered_queryset.annotate( + latest_scan_id=Subquery(latest_scan_subquery) + ) + + filtered_queryset = annotated_queryset.filter(scan_id=F("latest_scan_id")) + + severity_counts = ( + filtered_queryset.values("severity") + .annotate(count=Sum("total")) + .order_by("severity") + ) + + severity_data = {sev[0]: 0 for sev in SeverityChoices} + + for item in severity_counts: + severity_data[item["severity"]] = item["count"] + + serializer = OverviewSeveritySerializer(severity_data) + return Response(serializer.data, status=status.HTTP_200_OK) diff --git a/api/src/backend/tasks/jobs/scan.py b/api/src/backend/tasks/jobs/scan.py index 2b2fea8f17..5e84797377 100644 --- a/api/src/backend/tasks/jobs/scan.py +++ b/api/src/backend/tasks/jobs/scan.py @@ -3,8 +3,7 @@ from copy import deepcopy from datetime import datetime, timezone from celery.utils.log import get_task_logger -from prowler.lib.outputs.finding import Finding as ProwlerFinding -from prowler.lib.scan.scan import Scan as ProwlerScan +from django.db.models import Case, Count, IntegerField, Sum, When from api.compliance import ( PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE, @@ -12,17 +11,20 @@ from api.compliance import ( ) from api.db_utils import tenant_transaction from api.models import ( - Provider, - Scan, + ComplianceOverview, Finding, + Provider, Resource, ResourceTag, - StatusChoices as FindingStatus, + Scan, + ScanSummary, StateChoices, - ComplianceOverview, ) +from api.models import StatusChoices as FindingStatus from api.utils import initialize_prowler_provider from api.v1.serializers import ScanTaskSerializer +from prowler.lib.outputs.finding import Finding as ProwlerFinding +from prowler.lib.scan.scan import Scan as ProwlerScan logger = get_task_logger(__name__) @@ -268,7 +270,7 @@ def perform_prowler_scan( scan_instance.unique_resource_count = len(unique_resources) scan_instance.save() - if generate_compliance: + if exception is None and generate_compliance: try: regions = prowler_provider.get_regions() except AttributeError: @@ -321,3 +323,155 @@ def perform_prowler_scan( serializer = ScanTaskSerializer(instance=scan_instance) return serializer.data + + +def aggregate_findings(tenant_id: str, scan_id: str): + """ + Aggregates findings for a given scan and stores the results in the ScanSummary table. + + This function retrieves all findings associated with a given `scan_id` and calculates various + metrics such as counts of failed, passed, and muted findings, as well as their deltas (new, + changed, unchanged). The results are grouped by `check_id`, `service`, `severity`, and `region`. + These aggregated metrics are then stored in the `ScanSummary` table. + + Args: + tenant_id (str): The ID of the tenant to which the scan belongs. + scan_id (str): The ID of the scan for which findings need to be aggregated. + + Aggregated Metrics: + - fail: Total number of failed findings. + - _pass: Total number of passed findings. + - muted: Total number of muted findings. + - total: Total number of findings. + - new: Total number of new findings. + - changed: Total number of changed findings. + - unchanged: Total number of unchanged findings. + - fail_new: Failed findings with a delta of 'new'. + - fail_changed: Failed findings with a delta of 'changed'. + - pass_new: Passed findings with a delta of 'new'. + - pass_changed: Passed findings with a delta of 'changed'. + - muted_new: Muted findings with a delta of 'new'. + - muted_changed: Muted findings with a delta of 'changed'. + """ + with tenant_transaction(tenant_id): + findings = Finding.objects.filter(scan_id=scan_id) + + aggregation = findings.values( + "check_id", + "resources__service", + "severity", + "resources__region", + ).annotate( + fail=Sum( + Case( + When(status="FAIL", then=1), + default=0, + output_field=IntegerField(), + ) + ), + _pass=Sum( + Case( + When(status="PASS", then=1), + default=0, + output_field=IntegerField(), + ) + ), + muted=Sum( + Case( + When(status="MUTED", then=1), + default=0, + output_field=IntegerField(), + ) + ), + total=Count("id"), + new=Sum( + Case( + When(delta="new", then=1), + default=0, + output_field=IntegerField(), + ) + ), + changed=Sum( + Case( + When(delta="changed", then=1), + default=0, + output_field=IntegerField(), + ) + ), + unchanged=Sum( + Case( + When(delta__isnull=True, then=1), + default=0, + output_field=IntegerField(), + ) + ), + fail_new=Sum( + Case( + When(delta="new", status="FAIL", then=1), + default=0, + output_field=IntegerField(), + ) + ), + fail_changed=Sum( + Case( + When(delta="changed", status="FAIL", then=1), + default=0, + output_field=IntegerField(), + ) + ), + pass_new=Sum( + Case( + When(delta="new", status="PASS", then=1), + default=0, + output_field=IntegerField(), + ) + ), + pass_changed=Sum( + Case( + When(delta="changed", status="PASS", then=1), + default=0, + output_field=IntegerField(), + ) + ), + muted_new=Sum( + Case( + When(delta="new", status="MUTED", then=1), + default=0, + output_field=IntegerField(), + ) + ), + muted_changed=Sum( + Case( + When(delta="changed", status="MUTED", then=1), + default=0, + output_field=IntegerField(), + ) + ), + ) + + with tenant_transaction(tenant_id): + scan_aggregations = { + ScanSummary( + tenant_id=tenant_id, + scan_id=scan_id, + check_id=agg["check_id"], + service=agg["resources__service"], + severity=agg["severity"], + region=agg["resources__region"], + fail=agg["fail"], + _pass=agg["_pass"], + muted=agg["muted"], + total=agg["total"], + new=agg["new"], + changed=agg["changed"], + unchanged=agg["unchanged"], + fail_new=agg["fail_new"], + fail_changed=agg["fail_changed"], + pass_new=agg["pass_new"], + pass_changed=agg["pass_changed"], + muted_new=agg["muted_new"], + muted_changed=agg["muted_changed"], + ) + for agg in aggregation + } + ScanSummary.objects.bulk_create(scan_aggregations, batch_size=3000) diff --git a/api/src/backend/tasks/tasks.py b/api/src/backend/tasks/tasks.py index 4fc933127e..1237b543c5 100644 --- a/api/src/backend/tasks/tasks.py +++ b/api/src/backend/tasks/tasks.py @@ -1,12 +1,12 @@ from celery import shared_task +from config.celery import RLSTask +from tasks.jobs.connection import check_provider_connection +from tasks.jobs.deletion import delete_instance +from tasks.jobs.scan import aggregate_findings, perform_prowler_scan from api.db_utils import tenant_transaction from api.decorators import set_tenant from api.models import Provider, Scan -from config.celery import RLSTask -from tasks.jobs.connection import check_provider_connection -from tasks.jobs.deletion import delete_instance -from tasks.jobs.scan import perform_prowler_scan @shared_task(base=RLSTask, name="provider-connection-check") @@ -110,3 +110,8 @@ def perform_scheduled_scan_task(self, tenant_id: str, provider_id: str): scan_id=str(scan_instance.id), provider_id=provider_id, ) + + +@shared_task(name="scan-summary") +def perform_scan_summary_task(tenant_id: str, scan_id: str): + return aggregate_findings(tenant_id=tenant_id, scan_id=scan_id) diff --git a/api/src/backend/tasks/tests/test_scan.py b/api/src/backend/tasks/tests/test_scan.py index 798ebcb354..da79f78555 100644 --- a/api/src/backend/tasks/tests/test_scan.py +++ b/api/src/backend/tasks/tests/test_scan.py @@ -1,19 +1,19 @@ -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch import pytest - -from api.models import ( - StateChoices, - Severity, - Finding, - Resource, - StatusChoices, - Provider, -) from tasks.jobs.scan import ( - perform_prowler_scan, _create_finding_delta, _store_resources, + perform_prowler_scan, +) + +from api.models import ( + Finding, + Provider, + Resource, + Severity, + StateChoices, + StatusChoices, ) @@ -358,3 +358,6 @@ class TestPerformScan: assert resource == resource_instance assert resource_uid_tuple == (resource_instance.uid, resource_instance.region) + + +# TODO Add tests for aggregations From 17a39f3305507bf7ab188b3333e9b36dce481caf Mon Sep 17 00:00:00 2001 From: Sergio Garcia Date: Wed, 27 Nov 2024 11:54:59 -0400 Subject: [PATCH 14/56] fix(aws): exclude threat detection checks if category not present (#5933) --- prowler/lib/check/checks_loader.py | 2 +- tests/lib/check/check_loader_test.py | 76 ++++++++++++++++++++++++---- 2 files changed, 66 insertions(+), 12 deletions(-) diff --git a/prowler/lib/check/checks_loader.py b/prowler/lib/check/checks_loader.py index 8d5ac97ac4..ff54ccc8d1 100644 --- a/prowler/lib/check/checks_loader.py +++ b/prowler/lib/check/checks_loader.py @@ -111,7 +111,7 @@ def load_checks_to_execute( ): checks_to_execute.add(check_name) # Only execute threat detection checks if threat-detection category is set - if categories and categories != [] and "threat-detection" not in categories: + if not categories or "threat-detection" not in categories: for threat_detection_check in check_categories.get("threat-detection", []): checks_to_execute.discard(threat_detection_check) diff --git a/tests/lib/check/check_loader_test.py b/tests/lib/check/check_loader_test.py index 72080fc560..a122d76ff5 100644 --- a/tests/lib/check/check_loader_test.py +++ b/tests/lib/check/check_loader_test.py @@ -14,11 +14,13 @@ S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME_CUSTOM_ALIAS = ( S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_SEVERITY = "medium" S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME_SERVICE = "s3" +CLOUDTRAIL_THREAT_DETECTION_ENUMERATION_NAME = "cloudtrail_threat_detection_enumeration" + class TestCheckLoader: provider = "aws" - def get_custom_check_metadata(self): + def get_custom_check_s3_metadata(self): return CheckMetadata( Provider="aws", CheckID=S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME, @@ -52,9 +54,37 @@ class TestCheckLoader: Compliance=[], ) + def get_threat_detection_check_metadata(self): + return CheckMetadata( + Provider="aws", + CheckID=CLOUDTRAIL_THREAT_DETECTION_ENUMERATION_NAME, + CheckTitle="Ensure there are no potential enumeration threats in CloudTrail", + CheckType=[], + ServiceName="cloudtrail", + SubServiceName="", + ResourceIdTemplate="arn:partition:service:region:account-id:resource-id", + Severity="critical", + ResourceType="AwsCloudTrailTrail", + Description="This check ensures that there are no potential enumeration threats in CloudTrail.", + Risk="Potential enumeration threats in CloudTrail can lead to unauthorized access to resources.", + RelatedUrl="", + Remediation=Remediation( + Code=Code(CLI="", NativeIaC="", Other="", Terraform=""), + Recommendation=Recommendation( + Text="To remediate this issue, ensure that there are no potential enumeration threats in CloudTrail.", + Url="https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-concepts.html#cloudtrail-concepts-logging-data-events", + ), + ), + Categories=["threat-detection"], + DependsOn=[], + RelatedTo=[], + Notes="", + Compliance=[], + ) + def test_load_checks_to_execute(self): bulk_checks_metatada = { - S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME: self.get_custom_check_metadata() + S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME: self.get_custom_check_s3_metadata() } assert {S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME} == load_checks_to_execute( @@ -64,7 +94,7 @@ class TestCheckLoader: def test_load_checks_to_execute_with_check_list(self): bulk_checks_metatada = { - S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME: self.get_custom_check_metadata() + S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME: self.get_custom_check_s3_metadata() } check_list = [S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME] @@ -76,7 +106,7 @@ class TestCheckLoader: def test_load_checks_to_execute_with_severities(self): bulk_checks_metatada = { - S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME: self.get_custom_check_metadata() + S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME: self.get_custom_check_s3_metadata() } severities = [S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_SEVERITY] @@ -88,7 +118,7 @@ class TestCheckLoader: def test_load_checks_to_execute_with_severities_and_services(self): bulk_checks_metatada = { - S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME: self.get_custom_check_metadata() + S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME: self.get_custom_check_s3_metadata() } service_list = [S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME_SERVICE] severities = [S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_SEVERITY] @@ -104,7 +134,7 @@ class TestCheckLoader: self, ): bulk_checks_metatada = { - S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME: self.get_custom_check_metadata() + S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME: self.get_custom_check_s3_metadata() } service_list = ["ec2"] severities = [S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_SEVERITY] @@ -120,7 +150,7 @@ class TestCheckLoader: self, ): bulk_checks_metatada = { - S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME: self.get_custom_check_metadata() + S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME: self.get_custom_check_s3_metadata() } checks_file = "path/to/test_file" with patch( @@ -137,7 +167,7 @@ class TestCheckLoader: self, ): bulk_checks_metatada = { - S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME: self.get_custom_check_metadata() + S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME: self.get_custom_check_s3_metadata() } service_list = [S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME_SERVICE] @@ -178,7 +208,7 @@ class TestCheckLoader: self, ): bulk_checks_metatada = { - S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME: self.get_custom_check_metadata() + S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME: self.get_custom_check_s3_metadata() } categories = {"internet-exposed"} @@ -190,7 +220,7 @@ class TestCheckLoader: def test_load_checks_to_execute_no_bulk_checks_metadata(self): bulk_checks_metatada = { - S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME: self.get_custom_check_metadata() + S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME: self.get_custom_check_s3_metadata() } with patch( "prowler.lib.check.checks_loader.CheckMetadata.get_bulk", @@ -221,7 +251,7 @@ class TestCheckLoader: compliance_frameworks = ["soc2_aws"] bulk_checks_metatada = { - S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME: self.get_custom_check_metadata() + S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME: self.get_custom_check_s3_metadata() } with patch( "prowler.lib.check.checks_loader.CheckMetadata.get_bulk", @@ -248,3 +278,27 @@ class TestCheckLoader: assert {"check1_name", "check2_name"} == update_checks_to_execute_with_aliases( checks_to_execute, check_aliases ) + + def test_threat_detection_category(self): + bulk_checks_metatada = { + CLOUDTRAIL_THREAT_DETECTION_ENUMERATION_NAME: self.get_threat_detection_check_metadata() + } + categories = {"threat-detection"} + + assert {CLOUDTRAIL_THREAT_DETECTION_ENUMERATION_NAME} == load_checks_to_execute( + bulk_checks_metadata=bulk_checks_metatada, + categories=categories, + provider=self.provider, + ) + + def test_discard_threat_detection_checks(self): + bulk_checks_metatada = { + CLOUDTRAIL_THREAT_DETECTION_ENUMERATION_NAME: self.get_threat_detection_check_metadata() + } + categories = {} + + assert set() == load_checks_to_execute( + bulk_checks_metadata=bulk_checks_metatada, + categories=categories, + provider=self.provider, + ) From 4ba1c0259f0386b0bb5325453615bf7eebcb2ffc Mon Sep 17 00:00:00 2001 From: Sergio Garcia Date: Wed, 27 Nov 2024 14:00:26 -0400 Subject: [PATCH 15/56] fix(gcp): use session credentials to check if API is active (#5935) --- prowler/providers/gcp/lib/service/service.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/prowler/providers/gcp/lib/service/service.py b/prowler/providers/gcp/lib/service/service.py index dbb7f22638..9763230153 100644 --- a/prowler/providers/gcp/lib/service/service.py +++ b/prowler/providers/gcp/lib/service/service.py @@ -55,7 +55,9 @@ class GCPService: project_ids = [] for project_id in audited_project_ids: try: - client = discovery.build("serviceusage", "v1") + client = discovery.build( + "serviceusage", "v1", credentials=self.credentials + ) request = client.services().get( name=f"projects/{project_id}/services/{self.service}.googleapis.com" ) From fd8d34e8bc109024a28a13ed74cdd3c5e55abb18 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Thu, 28 Nov 2024 10:39:10 +0100 Subject: [PATCH 16/56] feat(ui:profile) add profile card (#5948) --- ui/actions/auth/auth.ts | 31 ++++++ ui/app/(prowler)/profile/page.tsx | 45 +++++---- .../filters/custom-region-selection.tsx | 32 +------ ui/components/users/profile/index.ts | 2 + .../users/profile/skeleton-user-info.tsx | 64 +++++++++++++ ui/components/users/profile/user-info.tsx | 77 +++++++++++++++ ui/lib/helper.ts | 94 +++++++++++++++++++ ui/types/components.ts | 26 +++++ 8 files changed, 322 insertions(+), 49 deletions(-) create mode 100644 ui/components/users/profile/index.ts create mode 100644 ui/components/users/profile/skeleton-user-info.tsx create mode 100644 ui/components/users/profile/user-info.tsx diff --git a/ui/actions/auth/auth.ts b/ui/actions/auth/auth.ts index 1bbc79b17e..19e0c62a38 100644 --- a/ui/actions/auth/auth.ts +++ b/ui/actions/auth/auth.ts @@ -1,9 +1,12 @@ "use server"; +import { revalidatePath } from "next/cache"; import { AuthError } from "next-auth"; import { z } from "zod"; import { signIn, signOut } from "@/auth.config"; +import { auth } from "@/auth.config"; +import { parseStringify } from "@/lib"; import { authFormSchema } from "@/types"; const formSchemaSignIn = authFormSchema("sign-in"); @@ -139,6 +142,34 @@ export const getToken = async (formData: z.infer) => { } }; +export const getProfileInfo = async () => { + const session = await auth(); + const keyServer = process.env.API_BASE_URL; + const url = new URL(`${keyServer}/users/me`); + + try { + const response = await fetch(url.toString(), { + method: "GET", + headers: { + Accept: "application/vnd.api+json", + Authorization: `Bearer ${session?.accessToken}`, + }, + }); + + if (!response.ok) { + throw new Error(`Failed to fetch user data: ${response.statusText}`); + } + + const data = await response.json(); + const parsedData = parseStringify(data); + revalidatePath("/profile"); + return parsedData; + } catch (error) { + console.error("Error fetching profile:", error); + return undefined; + } +}; + export const getUserByMe = async (accessToken: string) => { const keyServer = process.env.API_BASE_URL; const url = new URL(`${keyServer}/users/me`); diff --git a/ui/app/(prowler)/profile/page.tsx b/ui/app/(prowler)/profile/page.tsx index 0cfb4f8ab2..26d46a4380 100644 --- a/ui/app/(prowler)/profile/page.tsx +++ b/ui/app/(prowler)/profile/page.tsx @@ -1,30 +1,39 @@ import { Spacer } from "@nextui-org/react"; -import { redirect } from "next/navigation"; -import React from "react"; +import React, { Suspense } from "react"; -// import { getUserByMe } from "@/actions/auth/auth"; -import { auth } from "@/auth.config"; +import { getProfileInfo } from "@/actions/auth"; import { Header } from "@/components/ui"; +import { SkeletonUserInfo } from "@/components/users/profile"; +import { UserInfo } from "@/components/users/profile/user-info"; +import { UserProfileProps } from "@/types"; export default async function Profile() { - const session = await auth(); - - if (!session?.user) { - // redirect("/sign-in?returnTo=/profile"); - redirect("/sign-in"); - } - - // const user = await getUserByMe(); - return ( <>
- -
{JSON.stringify(session.user, null, 2)}
-
{JSON.stringify(session.userId, null, 2)}
-
{JSON.stringify(session.tenantId, null, 2)}
-
{JSON.stringify(session, null, 2)}
+
+
+
+
+ }> + + +
+
+
+
); } + +const SSRDataUser = async () => { + const userProfile: UserProfileProps = await getProfileInfo(); + + return ( + <> +

User Info

+ + + ); +}; diff --git a/ui/components/filters/custom-region-selection.tsx b/ui/components/filters/custom-region-selection.tsx index 7fc40fe0ea..f97179a5dd 100644 --- a/ui/components/filters/custom-region-selection.tsx +++ b/ui/components/filters/custom-region-selection.tsx @@ -4,37 +4,7 @@ import { Select, SelectItem } from "@nextui-org/react"; import { useRouter, useSearchParams } from "next/navigation"; import React, { useCallback, useMemo } from "react"; -const regions = [ - { key: "af-south-1", label: "AF South 1" }, - { key: "ap-east-1", label: "AP East 1" }, - { key: "ap-northeast-1", label: "AP Northeast 1" }, - { key: "ap-northeast-2", label: "AP Northeast 2" }, - { key: "ap-northeast-3", label: "AP Northeast 3" }, - { key: "ap-south-1", label: "AP South 1" }, - { key: "ap-south-2", label: "AP South 2" }, - { key: "ap-southeast-1", label: "AP Southeast 1" }, - { key: "ap-southeast-2", label: "AP Southeast 2" }, - { key: "ap-southeast-3", label: "AP Southeast 3" }, - { key: "ap-southeast-4", label: "AP Southeast 4" }, - { key: "ca-central-1", label: "CA Central 1" }, - { key: "ca-west-1", label: "CA West 1" }, - { key: "eu-central-1", label: "EU Central 1" }, - { key: "eu-central-2", label: "EU Central 2" }, - { key: "eu-north-1", label: "EU North 1" }, - { key: "eu-south-1", label: "EU South 1" }, - { key: "eu-south-2", label: "EU South 2" }, - { key: "eu-west-1", label: "EU West 1" }, - { key: "eu-west-2", label: "EU West 2" }, - { key: "eu-west-3", label: "EU West 3" }, - { key: "il-central-1", label: "IL Central 1" }, - { key: "me-central-1", label: "ME Central 1" }, - { key: "me-south-1", label: "ME South 1" }, - { key: "sa-east-1", label: "SA East 1" }, - { key: "us-east-1", label: "US East 1" }, - { key: "us-east-2", label: "US East 2" }, - { key: "us-west-1", label: "US West 1" }, - { key: "us-west-2", label: "US West 2" }, -]; +import { regions } from "@/lib/helper"; export const CustomRegionSelection: React.FC = () => { const router = useRouter(); diff --git a/ui/components/users/profile/index.ts b/ui/components/users/profile/index.ts new file mode 100644 index 0000000000..07aeca9b89 --- /dev/null +++ b/ui/components/users/profile/index.ts @@ -0,0 +1,2 @@ +export * from "./skeleton-user-info"; +export * from "./user-info"; diff --git a/ui/components/users/profile/skeleton-user-info.tsx b/ui/components/users/profile/skeleton-user-info.tsx new file mode 100644 index 0000000000..6bd52c322e --- /dev/null +++ b/ui/components/users/profile/skeleton-user-info.tsx @@ -0,0 +1,64 @@ +import { Card, CardBody, CardHeader, Skeleton } from "@nextui-org/react"; + +export const SkeletonUserInfo = () => { + const rows = 4; + + return ( + + + +
+
+
+ +
+ {/* Header Skeleton */} +
+ +
+
+ +
+
+ +
+
+ +
+
+
+ + {/* Row Skeletons */} + {Array.from({ length: rows }).map((_, index) => ( +
+ {/* Provider Name */} +
+ +
+
+ +
+
+
+ {/* Percent Passing */} + +
+
+ {/* Failing Checks */} + +
+
+ {/* Total Resources */} + +
+
+
+ ))} +
+
+
+ ); +}; diff --git a/ui/components/users/profile/user-info.tsx b/ui/components/users/profile/user-info.tsx new file mode 100644 index 0000000000..ac44770a04 --- /dev/null +++ b/ui/components/users/profile/user-info.tsx @@ -0,0 +1,77 @@ +"use client"; + +import { Card, CardBody } from "@nextui-org/react"; + +import { DateWithTime } from "@/components/ui/entities"; +import { UserProfileProps } from "@/types"; + +export const UserInfo = ({ + user, +}: { + user: UserProfileProps["data"] | null; +}) => { + if (!user || !user.attributes) { + return ( + + +
+
+

Name:

+ - +
+
+

Email:

+ - +
+
+

Company:

+ - +
+
+

+ Date Joined: +

+ - +
+
+
+ Unable to load user information. +
+ Please check your API connection. +
+
+
+ ); + } + + const { name, email, company_name, date_joined } = user.attributes; + + return ( + + +
+
+

Name:

+ {name} +
+
+

Email:

+ {email} +
+
+

Company:

+ {company_name} +
+
+

+ Date Joined: +

+ + + +
+
+
+
+ ); +}; diff --git a/ui/lib/helper.ts b/ui/lib/helper.ts index 5b6d2ec8f2..9c031b4e78 100644 --- a/ui/lib/helper.ts +++ b/ui/lib/helper.ts @@ -89,3 +89,97 @@ export const getErrorMessage = async (error: unknown): Promise => { } return message; }; + +export const regions = [ + // AWS Regions (ordered by usage) + { key: "us-east-1", label: "AWS - US East 1" }, + { key: "us-west-1", label: "AWS - US West 1" }, + { key: "us-west-2", label: "AWS - US West 2" }, + { key: "eu-west-1", label: "AWS - EU West 1" }, + { key: "eu-central-1", label: "AWS - EU Central 1" }, + { key: "ap-southeast-1", label: "AWS - AP Southeast 1" }, + { key: "ap-northeast-1", label: "AWS - AP Northeast 1" }, + { key: "ap-southeast-2", label: "AWS - AP Southeast 2" }, + { key: "ca-central-1", label: "AWS - CA Central 1" }, + { key: "sa-east-1", label: "AWS - SA East 1" }, + { key: "af-south-1", label: "AWS - AF South 1" }, + { key: "ap-east-1", label: "AWS - AP East 1" }, + { key: "ap-northeast-2", label: "AWS - AP Northeast 2" }, + { key: "ap-northeast-3", label: "AWS - AP Northeast 3" }, + { key: "ap-south-1", label: "AWS - AP South 1" }, + { key: "ap-south-2", label: "AWS - AP South 2" }, + { key: "ap-southeast-3", label: "AWS - AP Southeast 3" }, + { key: "ap-southeast-4", label: "AWS - AP Southeast 4" }, + { key: "ca-west-1", label: "AWS - CA West 1" }, + { key: "eu-central-2", label: "AWS - EU Central 2" }, + { key: "eu-north-1", label: "AWS - EU North 1" }, + { key: "eu-south-1", label: "AWS - EU South 1" }, + { key: "eu-south-2", label: "AWS - EU South 2" }, + { key: "eu-west-2", label: "AWS - EU West 2" }, + { key: "eu-west-3", label: "AWS - EU West 3" }, + { key: "il-central-1", label: "AWS - IL Central 1" }, + { key: "me-central-1", label: "AWS - ME Central 1" }, + { key: "me-south-1", label: "AWS - ME South 1" }, + + // Azure Regions (ordered by usage) + { key: "eastus", label: "Azure - East US" }, + { key: "eastus2", label: "Azure - East US 2" }, + { key: "westeurope", label: "Azure - West Europe" }, + { key: "southeastasia", label: "Azure - Southeast Asia" }, + { key: "uksouth", label: "Azure - UK South" }, + { key: "northeurope", label: "Azure - North Europe" }, + { key: "centralus", label: "Azure - Central US" }, + { key: "westus2", label: "Azure - West US 2" }, + { key: "southcentralus", label: "Azure - South Central US" }, + { key: "australiaeast", label: "Azure - Australia East" }, + { key: "canadacentral", label: "Azure - Canada Central" }, + { key: "japaneast", label: "Azure - Japan East" }, + { key: "koreacentral", label: "Azure - Korea Central" }, + { key: "southafricanorth", label: "Azure - South Africa North" }, + { key: "brazilsouth", label: "Azure - Brazil South" }, + { key: "francecentral", label: "Azure - France Central" }, + { key: "germanywestcentral", label: "Azure - Germany West Central" }, + { key: "switzerlandnorth", label: "Azure - Switzerland North" }, + { key: "uaenorth", label: "Azure - UAE North" }, + // Remaining Azure Regions (less frequently used) + { key: "westus", label: "Azure - West US" }, + { key: "northcentralus", label: "Azure - North Central US" }, + { key: "australiasoutheast", label: "Azure - Australia Southeast" }, + { key: "southindia", label: "Azure - South India" }, + { key: "westindia", label: "Azure - West India" }, + { key: "canadaeast", label: "Azure - Canada East" }, + { key: "francesouth", label: "Azure - France South" }, + { key: "norwayeast", label: "Azure - Norway East" }, + { key: "switzerlandwest", label: "Azure - Switzerland West" }, + { key: "ukwest", label: "Azure - UK West" }, + { key: "uaecentral", label: "Azure - UAE Central" }, + { key: "brazilsoutheast", label: "Azure - Brazil Southeast" }, + + // GCP Regions (ordered by usage) + { key: "us-central1", label: "GCP - US Central (Iowa)" }, + { key: "us-east1", label: "GCP - US East (South Carolina)" }, + { key: "us-west1", label: "GCP - US West (Oregon)" }, + { key: "europe-west1", label: "GCP - Europe West (Belgium)" }, + { key: "asia-east1", label: "GCP - Asia East (Taiwan)" }, + { key: "asia-northeast1", label: "GCP - Asia Northeast (Tokyo)" }, + { key: "europe-west2", label: "GCP - Europe West (London)" }, + { key: "europe-west3", label: "GCP - Europe West (Frankfurt)" }, + { key: "europe-west4", label: "GCP - Europe West (Netherlands)" }, + { key: "asia-southeast1", label: "GCP - Asia Southeast (Singapore)" }, + { key: "australia-southeast1", label: "GCP - Australia Southeast (Sydney)" }, + { + key: "northamerica-northeast1", + label: "GCP - North America Northeast (Montreal)", + }, + // Remaining GCP Regions + { key: "asia-east2", label: "GCP - Asia East (Hong Kong)" }, + { key: "asia-northeast2", label: "GCP - Asia Northeast (Osaka)" }, + { key: "asia-northeast3", label: "GCP - Asia Northeast (Seoul)" }, + { key: "asia-south1", label: "GCP - Asia South (Mumbai)" }, + { key: "asia-southeast2", label: "GCP - Asia Southeast (Jakarta)" }, + { key: "europe-north1", label: "GCP - Europe North (Finland)" }, + { key: "europe-west6", label: "GCP - Europe West (Zurich)" }, + { key: "southamerica-east1", label: "GCP - South America East (São Paulo)" }, + { key: "us-west2", label: "GCP - US West (Los Angeles)" }, + { key: "us-east4", label: "GCP - US East (Northern Virginia)" }, +]; diff --git a/ui/types/components.ts b/ui/types/components.ts index ffe10216e0..0eca498945 100644 --- a/ui/types/components.ts +++ b/ui/types/components.ts @@ -227,6 +227,32 @@ export interface InvitationProps { self: string; }; } +export interface UserProfileProps { + data: { + type: "users"; + id: string; + attributes: { + name: string; + email: string; + company_name: string; + date_joined: string; + }; + relationships: { + memberships: { + meta: { + count: number; + }; + data: Array<{ + type: "memberships"; + id: string; + }>; + }; + }; + }; + meta: { + version: string; + }; +} export interface UserProps { type: "users"; From d5187b3099d942ae4ae50e25427289bce69dc12d Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Thu, 28 Nov 2024 12:55:31 +0100 Subject: [PATCH 17/56] chore(auth): restore auth file and move the server action to user file (#5951) --- ui/actions/auth/auth.ts | 31 ------------------------------- ui/actions/users/users.ts | 28 ++++++++++++++++++++++++++++ ui/app/(prowler)/profile/page.tsx | 2 +- 3 files changed, 29 insertions(+), 32 deletions(-) diff --git a/ui/actions/auth/auth.ts b/ui/actions/auth/auth.ts index 19e0c62a38..1bbc79b17e 100644 --- a/ui/actions/auth/auth.ts +++ b/ui/actions/auth/auth.ts @@ -1,12 +1,9 @@ "use server"; -import { revalidatePath } from "next/cache"; import { AuthError } from "next-auth"; import { z } from "zod"; import { signIn, signOut } from "@/auth.config"; -import { auth } from "@/auth.config"; -import { parseStringify } from "@/lib"; import { authFormSchema } from "@/types"; const formSchemaSignIn = authFormSchema("sign-in"); @@ -142,34 +139,6 @@ export const getToken = async (formData: z.infer) => { } }; -export const getProfileInfo = async () => { - const session = await auth(); - const keyServer = process.env.API_BASE_URL; - const url = new URL(`${keyServer}/users/me`); - - try { - const response = await fetch(url.toString(), { - method: "GET", - headers: { - Accept: "application/vnd.api+json", - Authorization: `Bearer ${session?.accessToken}`, - }, - }); - - if (!response.ok) { - throw new Error(`Failed to fetch user data: ${response.statusText}`); - } - - const data = await response.json(); - const parsedData = parseStringify(data); - revalidatePath("/profile"); - return parsedData; - } catch (error) { - console.error("Error fetching profile:", error); - return undefined; - } -}; - export const getUserByMe = async (accessToken: string) => { const keyServer = process.env.API_BASE_URL; const url = new URL(`${keyServer}/users/me`); diff --git a/ui/actions/users/users.ts b/ui/actions/users/users.ts index 30a5b26d1f..cf3397a940 100644 --- a/ui/actions/users/users.ts +++ b/ui/actions/users/users.ts @@ -114,3 +114,31 @@ export const deleteUser = async (formData: FormData) => { }; } }; + +export const getProfileInfo = async () => { + const session = await auth(); + const keyServer = process.env.API_BASE_URL; + const url = new URL(`${keyServer}/users/me`); + + try { + const response = await fetch(url.toString(), { + method: "GET", + headers: { + Accept: "application/vnd.api+json", + Authorization: `Bearer ${session?.accessToken}`, + }, + }); + + if (!response.ok) { + throw new Error(`Failed to fetch user data: ${response.statusText}`); + } + + const data = await response.json(); + const parsedData = parseStringify(data); + revalidatePath("/profile"); + return parsedData; + } catch (error) { + console.error("Error fetching profile:", error); + return undefined; + } +}; diff --git a/ui/app/(prowler)/profile/page.tsx b/ui/app/(prowler)/profile/page.tsx index 26d46a4380..e09aeae757 100644 --- a/ui/app/(prowler)/profile/page.tsx +++ b/ui/app/(prowler)/profile/page.tsx @@ -1,7 +1,7 @@ import { Spacer } from "@nextui-org/react"; import React, { Suspense } from "react"; -import { getProfileInfo } from "@/actions/auth"; +import { getProfileInfo } from "@/actions/users/users"; import { Header } from "@/components/ui"; import { SkeletonUserInfo } from "@/components/users/profile"; import { UserInfo } from "@/components/users/profile/user-info"; From af815287ed412abcc551d1f5f6f8ec9ae031fc9d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 28 Nov 2024 09:11:17 -0400 Subject: [PATCH 18/56] chore(deps-dev): bump bandit from 1.7.10 to 1.8.0 (#5943) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- poetry.lock | 10 +++++----- pyproject.toml | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/poetry.lock b/poetry.lock index 3bfa70b6cf..ea4545aaf5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -694,13 +694,13 @@ dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] [[package]] name = "bandit" -version = "1.7.10" +version = "1.8.0" description = "Security oriented static analyser for python code." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "bandit-1.7.10-py3-none-any.whl", hash = "sha256:665721d7bebbb4485a339c55161ac0eedde27d51e638000d91c8c2d68343ad02"}, - {file = "bandit-1.7.10.tar.gz", hash = "sha256:59ed5caf5d92b6ada4bf65bc6437feea4a9da1093384445fed4d472acc6cff7b"}, + {file = "bandit-1.8.0-py3-none-any.whl", hash = "sha256:b1a61d829c0968aed625381e426aa378904b996529d048f8d908fa28f6b13e38"}, + {file = "bandit-1.8.0.tar.gz", hash = "sha256:b5bfe55a095abd9fe20099178a7c6c060f844bfd4fe4c76d28e35e4c52b9d31e"}, ] [package.dependencies] @@ -5194,4 +5194,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.13" -content-hash = "6a868e7040647c561274f7bb54dd5a899690d2b8dc658559c9de7e175debee6e" +content-hash = "1c4e3f619bfc461c022448d3a43775baf01bcbd8f5972ae4f394da2e059fdb2c" diff --git a/pyproject.toml b/pyproject.toml index 582896a6c6..13a21d1641 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -75,7 +75,7 @@ tabulate = "0.9.0" tzlocal = "5.2" [tool.poetry.group.dev.dependencies] -bandit = "1.7.10" +bandit = "1.8.0" black = "24.10.0" coverage = "7.6.8" docker = "7.1.0" From 70e327a3c11018b3a1ba429089b3c8843a4f3cc7 Mon Sep 17 00:00:00 2001 From: Prowler Bot Date: Thu, 28 Nov 2024 14:11:29 +0100 Subject: [PATCH 19/56] chore(regions_update): Changes in regions for AWS services (#5947) Co-authored-by: sergargar <38561120+sergargar@users.noreply.github.com> --- prowler/providers/aws/aws_regions_by_service.json | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/prowler/providers/aws/aws_regions_by_service.json b/prowler/providers/aws/aws_regions_by_service.json index f8ec725446..cfc75a2e06 100644 --- a/prowler/providers/aws/aws_regions_by_service.json +++ b/prowler/providers/aws/aws_regions_by_service.json @@ -9265,10 +9265,7 @@ "us-west-2" ], "aws-cn": [], - "aws-us-gov": [ - "us-gov-east-1", - "us-gov-west-1" - ] + "aws-us-gov": [] } }, "sagemaker-runtime": { From 9bf3171cfa0c28f287619348ff362a43695bf32b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 28 Nov 2024 11:57:35 -0400 Subject: [PATCH 20/56] chore(deps): bump botocore from 1.35.70 to 1.35.71 (#5944) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- poetry.lock | 8 ++++---- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/poetry.lock b/poetry.lock index ea4545aaf5..daa6170f55 100644 --- a/poetry.lock +++ b/poetry.lock @@ -794,13 +794,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.35.70" +version = "1.35.71" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">=3.8" files = [ - {file = "botocore-1.35.70-py3-none-any.whl", hash = "sha256:ba8a4797cf7c5d9c237e67a62692f5146e895613fd3e6a43b00b66f3a8c7fc73"}, - {file = "botocore-1.35.70.tar.gz", hash = "sha256:18d1bb505722d9efd50c50719ed8de7284bfe6d3908a9e08756a7646e549da21"}, + {file = "botocore-1.35.71-py3-none-any.whl", hash = "sha256:fc46e7ab1df3cef66dfba1633f4da77c75e07365b36f03bd64a3793634be8fc1"}, + {file = "botocore-1.35.71.tar.gz", hash = "sha256:f9fa058e0393660c3fe53c1e044751beb64b586def0bd2212448a7c328b0cbba"}, ] [package.dependencies] @@ -5194,4 +5194,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.13" -content-hash = "1c4e3f619bfc461c022448d3a43775baf01bcbd8f5972ae4f394da2e059fdb2c" +content-hash = "6e51c3d50d88e8bb5d91b7ace1aa07d599fad4b151465355e51c573a5ad41d85" diff --git a/pyproject.toml b/pyproject.toml index 13a21d1641..c0eab018ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,7 +49,7 @@ azure-mgmt-subscription = "3.1.1" azure-mgmt-web = "7.3.1" azure-storage-blob = "12.24.0" boto3 = "1.35.70" -botocore = "1.35.70" +botocore = "1.35.71" colorama = "0.4.6" cryptography = "43.0.1" dash = "2.18.2" From bcf1ef1d31364da930007f6b652dbbed11b9e5de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Mart=C3=ADn?= Date: Thu, 28 Nov 2024 18:06:06 +0100 Subject: [PATCH 21/56] chore(check): remove custom_report_interface (#5955) --- prowler/lib/check/check.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/prowler/lib/check/check.py b/prowler/lib/check/check.py index fc648ffc04..d760702fa8 100644 --- a/prowler/lib/check/check.py +++ b/prowler/lib/check/check.py @@ -556,19 +556,6 @@ def execute_checks( bar() bar.title = f"-> {Fore.GREEN}Scan completed!{Style.RESET_ALL}" - # Custom report interface - if os.environ.get("PROWLER_REPORT_LIB_PATH"): - try: - logger.info("Using custom report interface ...") - lib = os.environ["PROWLER_REPORT_LIB_PATH"] - outputs_module = importlib.import_module(lib) - custom_report_interface = getattr(outputs_module, "report") - - # TODO: review this call and see if we can remove the global_provider.output_options since it is contained in the global_provider - custom_report_interface(check_findings, output_options, global_provider) - except Exception: - sys.exit(1) - return all_findings From 6dea923866a3ab77b58e3fd3c8457f6bb67fb254 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Fri, 29 Nov 2024 06:54:38 +0100 Subject: [PATCH 22/56] chore(codebase) Update/UI code base (#5960) --- ui/actions/auth/auth.ts | 37 +++++++--- ui/app/(prowler)/compliance/page.tsx | 2 + ui/auth.config.ts | 1 + ui/components/auth/oss/auth-form.tsx | 12 ++- ui/components/compliance/compliance-card.tsx | 13 +++- .../data-compliance/data-compliance.tsx | 10 ++- .../via-credentials/k8s-credentials-form.tsx | 10 +-- ui/components/ui/custom/custom-textarea.tsx | 74 +++++++++++++++++++ ui/components/ui/custom/index.ts | 1 + 9 files changed, 138 insertions(+), 22 deletions(-) create mode 100644 ui/components/ui/custom/custom-textarea.tsx diff --git a/ui/actions/auth/auth.ts b/ui/actions/auth/auth.ts index 1bbc79b17e..d39314d2da 100644 --- a/ui/actions/auth/auth.ts +++ b/ui/actions/auth/auth.ts @@ -37,6 +37,10 @@ export async function authenticate( credentials: "Incorrect email or password", }, }; + case "CallbackRouteError": + return { + message: error.cause?.err?.message, + }; default: return { message: "Unknown error", @@ -152,22 +156,31 @@ export const getUserByMe = async (accessToken: string) => { }, }); - if (!response.ok) throw new Error("Error in trying to get user by me"); - const parsedResponse = await response.json(); + if (!response.ok) { + // Handle different HTTP error codes + switch (response.status) { + case 401: + throw new Error("Invalid or expired token"); + case 403: + throw new Error(parsedResponse.errors?.[0]?.detail); + case 404: + throw new Error("User not found"); + default: + throw new Error( + parsedResponse.errors?.[0]?.detail || "Unknown error", + ); + } + } - const name = parsedResponse.data.attributes.name; - const email = parsedResponse.data.attributes.email; - const company = parsedResponse.data.attributes.company_name; - const dateJoined = parsedResponse.data.attributes.date_joined; return { - name, - email, - company, - dateJoined, + name: parsedResponse.data.attributes.name, + email: parsedResponse.data.attributes.email, + company: parsedResponse.data.attributes.company_name, + dateJoined: parsedResponse.data.attributes.date_joined, }; - } catch (error) { - throw new Error("Error in trying to get user by me"); + } catch (error: any) { + throw new Error(error.message || "Network error or server unreachable"); } }; diff --git a/ui/app/(prowler)/compliance/page.tsx b/ui/app/(prowler)/compliance/page.tsx index 1bc58ad741..558caeaf9b 100644 --- a/ui/app/(prowler)/compliance/page.tsx +++ b/ui/app/(prowler)/compliance/page.tsx @@ -140,6 +140,7 @@ const SSRComplianceGrid = async ({ const { attributes } = compliance; const { framework, + version, requirements_status: { passed, total }, } = attributes; @@ -147,6 +148,7 @@ const SSRComplianceGrid = async ({ { const formSchema = authFormSchema(type); const router = useRouter(); @@ -47,7 +49,6 @@ export const AuthForm = ({ email: data.email.toLowerCase(), password: data.password, }); - if (result?.message === "Success") { router.push("/"); } else if (result?.errors && "credentials" in result.errors) { @@ -55,6 +56,8 @@ export const AuthForm = ({ type: "server", message: result.errors.credentials ?? "Incorrect email or password", }); + } else if (result?.message === "User email is not verified") { + router.push("/email-verification"); } else { toast({ variant: "destructive", @@ -73,7 +76,12 @@ export const AuthForm = ({ description: "The user was registered successfully.", }); form.reset(); - router.push("/sign-in"); + + if (isCloudEnv) { + router.push("/email-verification"); + } else { + router.push("/sign-in"); + } } else { newUser.errors.forEach((error: ApiError) => { const errorMessage = error.detail; diff --git a/ui/components/compliance/compliance-card.tsx b/ui/components/compliance/compliance-card.tsx index 7fcfdbf5dd..b1b8d8ec10 100644 --- a/ui/components/compliance/compliance-card.tsx +++ b/ui/components/compliance/compliance-card.tsx @@ -6,6 +6,7 @@ import { getComplianceIcon } from "../icons"; interface ComplianceCardProps { title: string; + version: string; passingRequirements: number; totalRequirements: number; prevPassingRequirements: number; @@ -14,9 +15,14 @@ interface ComplianceCardProps { export const ComplianceCard: React.FC = ({ title, + version, passingRequirements, totalRequirements, }) => { + const formatTitle = (title: string) => { + return title.split("-").join(" "); + }; + const ratingPercentage = Math.floor( (passingRequirements / totalRequirements) * 100, ); @@ -47,7 +53,7 @@ export const ComplianceCard: React.FC = ({ }; return ( - +
= ({ className="h-10 w-10 min-w-10 rounded-md border-1 border-gray-300 bg-white object-contain p-1" />
-

{title}

+

+ {formatTitle(title)} + {version ? ` - ${version}` : ""} +

{ const searchParams = useSearchParams(); const [showClearButton, setShowClearButton] = useState(false); const scanIdParam = searchParams.get("scanId"); - const selectedScanId = scanIdParam || scans[0]?.id; + const selectedScanId = scanIdParam || (scans.length > 0 ? scans[0].id : ""); + + useEffect(() => { + if (!scanIdParam && scans.length > 0) { + const params = new URLSearchParams(searchParams); + params.set("scanId", scans[0].id); + router.push(`?${params.toString()}`); + } + }, [scans, scanIdParam, searchParams, router]); useEffect(() => { const hasFilters = Array.from(searchParams.keys()).some( 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 0725d134eb..3e6caeca88 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 @@ -1,6 +1,6 @@ import { Control } from "react-hook-form"; -import { CustomInput } from "@/components/ui/custom"; +import { CustomTextarea } from "@/components/ui/custom"; import { KubernetesCredentials } from "@/types"; export const KubernetesCredentialsForm = ({ @@ -15,17 +15,17 @@ export const KubernetesCredentialsForm = ({ Connect via Credentials
- Please provide the information for your Kubernetes credentials. + Please provide the kubeconfig content for your Kubernetes credentials.
- diff --git a/ui/components/ui/custom/custom-textarea.tsx b/ui/components/ui/custom/custom-textarea.tsx new file mode 100644 index 0000000000..c3f6dc0994 --- /dev/null +++ b/ui/components/ui/custom/custom-textarea.tsx @@ -0,0 +1,74 @@ +"use client"; + +import { Textarea } from "@nextui-org/input"; +import React from "react"; +import { Control, FieldPath, FieldValues } from "react-hook-form"; + +import { FormControl, FormField, FormMessage } from "@/components/ui/form"; + +interface CustomTextareaProps { + control: Control; + name: FieldPath; + label?: string; + labelPlacement?: "inside" | "outside" | "outside-left"; + variant?: "flat" | "bordered" | "underlined" | "faded"; + size?: "sm" | "md" | "lg"; + placeholder?: string; + defaultValue?: string; + isRequired?: boolean; + isInvalid?: boolean; + minRows?: number; + maxRows?: number; + fullWidth?: boolean; + disableAutosize?: boolean; + description?: React.ReactNode; +} + +export const CustomTextarea = ({ + control, + name, + label = name, + labelPlacement = "inside", + placeholder, + variant = "flat", + size = "md", + defaultValue, + isRequired = false, + isInvalid = false, + minRows = 3, + maxRows = 8, + fullWidth = true, + disableAutosize = false, + description, +}: CustomTextareaProps) => { + return ( + ( + <> + +