mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-04-16 01:28:26 +00:00
Compare commits
4 Commits
add-fallba
...
5.24.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b45724ca8 | ||
|
|
ba5b23245f | ||
|
|
43913b1592 | ||
|
|
9e31160887 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -84,6 +84,7 @@ continue.json
|
||||
.continuerc.json
|
||||
|
||||
# AI Coding Assistants - OpenCode
|
||||
.opencode/
|
||||
opencode.json
|
||||
|
||||
# AI Coding Assistants - GitHub Copilot
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
All notable changes to the **Prowler API** are documented in this file.
|
||||
|
||||
## [1.25.0] (Prowler UNRELEASED)
|
||||
## [1.25.0] (Prowler v5.24.0)
|
||||
|
||||
### 🔄 Changed
|
||||
|
||||
|
||||
166
api/poetry.lock
generated
166
api/poetry.lock
generated
@@ -682,21 +682,21 @@ requests = ">=2.21.0,<3.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "alibabacloud-tea-openapi"
|
||||
version = "0.4.1"
|
||||
version = "0.4.4"
|
||||
description = "Alibaba Cloud openapi SDK Library for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "alibabacloud_tea_openapi-0.4.1-py3-none-any.whl", hash = "sha256:e46bfa3ca34086d2c357d217a0b7284ecbd4b3bab5c88e075e73aec637b0e4a0"},
|
||||
{file = "alibabacloud_tea_openapi-0.4.1.tar.gz", hash = "sha256:2384b090870fdb089c3c40f3fb8cf0145b8c7d6c14abbac521f86a01abb5edaf"},
|
||||
{file = "alibabacloud_tea_openapi-0.4.4-py3-none-any.whl", hash = "sha256:cea6bc1fe35b0319a8752cb99eb0ecb0dab7ca1a71b99c12970ba0867410995f"},
|
||||
{file = "alibabacloud_tea_openapi-0.4.4.tar.gz", hash = "sha256:1b0917bc03cd49417da64945e92731716d53e2eb8707b235f54e45b7473221ce"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
alibabacloud-credentials = ">=1.0.2,<2.0.0"
|
||||
alibabacloud-gateway-spi = ">=0.0.2,<1.0.0"
|
||||
alibabacloud-tea-util = ">=0.3.13,<1.0.0"
|
||||
cryptography = ">=3.0.0,<45.0.0"
|
||||
cryptography = {version = ">=3.0.0,<47.0.0", markers = "python_version >= \"3.8\""}
|
||||
darabonba-core = ">=1.0.3,<2.0.0"
|
||||
|
||||
[[package]]
|
||||
@@ -2503,62 +2503,74 @@ dev = ["bandit", "coverage", "flake8", "pydocstyle", "pylint", "pytest", "pytest
|
||||
|
||||
[[package]]
|
||||
name = "cryptography"
|
||||
version = "44.0.3"
|
||||
version = "46.0.6"
|
||||
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
|
||||
optional = false
|
||||
python-versions = "!=3.9.0,!=3.9.1,>=3.7"
|
||||
python-versions = "!=3.9.0,!=3.9.1,>=3.8"
|
||||
groups = ["main", "dev"]
|
||||
files = [
|
||||
{file = "cryptography-44.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:962bc30480a08d133e631e8dfd4783ab71cc9e33d5d7c1e192f0b7c06397bb88"},
|
||||
{file = "cryptography-44.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffc61e8f3bf5b60346d89cd3d37231019c17a081208dfbbd6e1605ba03fa137"},
|
||||
{file = "cryptography-44.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58968d331425a6f9eedcee087f77fd3c927c88f55368f43ff7e0a19891f2642c"},
|
||||
{file = "cryptography-44.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e28d62e59a4dbd1d22e747f57d4f00c459af22181f0b2f787ea83f5a876d7c76"},
|
||||
{file = "cryptography-44.0.3-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af653022a0c25ef2e3ffb2c673a50e5a0d02fecc41608f4954176f1933b12359"},
|
||||
{file = "cryptography-44.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:157f1f3b8d941c2bd8f3ffee0af9b049c9665c39d3da9db2dc338feca5e98a43"},
|
||||
{file = "cryptography-44.0.3-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:c6cd67722619e4d55fdb42ead64ed8843d64638e9c07f4011163e46bc512cf01"},
|
||||
{file = "cryptography-44.0.3-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b424563394c369a804ecbee9b06dfb34997f19d00b3518e39f83a5642618397d"},
|
||||
{file = "cryptography-44.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c91fc8e8fd78af553f98bc7f2a1d8db977334e4eea302a4bfd75b9461c2d8904"},
|
||||
{file = "cryptography-44.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:25cd194c39fa5a0aa4169125ee27d1172097857b27109a45fadc59653ec06f44"},
|
||||
{file = "cryptography-44.0.3-cp37-abi3-win32.whl", hash = "sha256:3be3f649d91cb182c3a6bd336de8b61a0a71965bd13d1a04a0e15b39c3d5809d"},
|
||||
{file = "cryptography-44.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:3883076d5c4cc56dbef0b898a74eb6992fdac29a7b9013870b34efe4ddb39a0d"},
|
||||
{file = "cryptography-44.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:5639c2b16764c6f76eedf722dbad9a0914960d3489c0cc38694ddf9464f1bb2f"},
|
||||
{file = "cryptography-44.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3ffef566ac88f75967d7abd852ed5f182da252d23fac11b4766da3957766759"},
|
||||
{file = "cryptography-44.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:192ed30fac1728f7587c6f4613c29c584abdc565d7417c13904708db10206645"},
|
||||
{file = "cryptography-44.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7d5fe7195c27c32a64955740b949070f21cba664604291c298518d2e255931d2"},
|
||||
{file = "cryptography-44.0.3-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3f07943aa4d7dad689e3bb1638ddc4944cc5e0921e3c227486daae0e31a05e54"},
|
||||
{file = "cryptography-44.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cb90f60e03d563ca2445099edf605c16ed1d5b15182d21831f58460c48bffb93"},
|
||||
{file = "cryptography-44.0.3-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ab0b005721cc0039e885ac3503825661bd9810b15d4f374e473f8c89b7d5460c"},
|
||||
{file = "cryptography-44.0.3-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:3bb0847e6363c037df8f6ede57d88eaf3410ca2267fb12275370a76f85786a6f"},
|
||||
{file = "cryptography-44.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b0cc66c74c797e1db750aaa842ad5b8b78e14805a9b5d1348dc603612d3e3ff5"},
|
||||
{file = "cryptography-44.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6866df152b581f9429020320e5eb9794c8780e90f7ccb021940d7f50ee00ae0b"},
|
||||
{file = "cryptography-44.0.3-cp39-abi3-win32.whl", hash = "sha256:c138abae3a12a94c75c10499f1cbae81294a6f983b3af066390adee73f433028"},
|
||||
{file = "cryptography-44.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:5d186f32e52e66994dce4f766884bcb9c68b8da62d61d9d215bfe5fb56d21334"},
|
||||
{file = "cryptography-44.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:cad399780053fb383dc067475135e41c9fe7d901a97dd5d9c5dfb5611afc0d7d"},
|
||||
{file = "cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:21a83f6f35b9cc656d71b5de8d519f566df01e660ac2578805ab245ffd8523f8"},
|
||||
{file = "cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fc3c9babc1e1faefd62704bb46a69f359a9819eb0292e40df3fb6e3574715cd4"},
|
||||
{file = "cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:e909df4053064a97f1e6565153ff8bb389af12c5c8d29c343308760890560aff"},
|
||||
{file = "cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:dad80b45c22e05b259e33ddd458e9e2ba099c86ccf4e88db7bbab4b747b18d06"},
|
||||
{file = "cryptography-44.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:479d92908277bed6e1a1c69b277734a7771c2b78633c224445b5c60a9f4bc1d9"},
|
||||
{file = "cryptography-44.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:896530bc9107b226f265effa7ef3f21270f18a2026bc09fed1ebd7b66ddf6375"},
|
||||
{file = "cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:9b4d4a5dbee05a2c390bf212e78b99434efec37b17a4bff42f50285c5c8c9647"},
|
||||
{file = "cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02f55fb4f8b79c1221b0961488eaae21015b69b210e18c386b69de182ebb1259"},
|
||||
{file = "cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:dd3db61b8fe5be220eee484a17233287d0be6932d056cf5738225b9c05ef4fff"},
|
||||
{file = "cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:978631ec51a6bbc0b7e58f23b68a8ce9e5f09721940933e9c217068388789fe5"},
|
||||
{file = "cryptography-44.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:5d20cc348cca3a8aa7312f42ab953a56e15323800ca3ab0706b8cd452a3a056c"},
|
||||
{file = "cryptography-44.0.3.tar.gz", hash = "sha256:fe19d8bc5536a91a24a8133328880a41831b6c5df54599a8417b62fe015d3053"},
|
||||
{file = "cryptography-46.0.6-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:64235194bad039a10bb6d2d930ab3323baaec67e2ce36215fd0952fad0930ca8"},
|
||||
{file = "cryptography-46.0.6-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:26031f1e5ca62fcb9d1fcb34b2b60b390d1aacaa15dc8b895a9ed00968b97b30"},
|
||||
{file = "cryptography-46.0.6-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9a693028b9cbe51b5a1136232ee8f2bc242e4e19d456ded3fa7c86e43c713b4a"},
|
||||
{file = "cryptography-46.0.6-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:67177e8a9f421aa2d3a170c3e56eca4e0128883cf52a071a7cbf53297f18b175"},
|
||||
{file = "cryptography-46.0.6-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d9528b535a6c4f8ff37847144b8986a9a143585f0540fbcb1a98115b543aa463"},
|
||||
{file = "cryptography-46.0.6-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:22259338084d6ae497a19bae5d4c66b7ca1387d3264d1c2c0e72d9e9b6a77b97"},
|
||||
{file = "cryptography-46.0.6-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:760997a4b950ff00d418398ad73fbc91aa2894b5c1db7ccb45b4f68b42a63b3c"},
|
||||
{file = "cryptography-46.0.6-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3dfa6567f2e9e4c5dceb8ccb5a708158a2a871052fa75c8b78cb0977063f1507"},
|
||||
{file = "cryptography-46.0.6-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:cdcd3edcbc5d55757e5f5f3d330dd00007ae463a7e7aa5bf132d1f22a4b62b19"},
|
||||
{file = "cryptography-46.0.6-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:d4e4aadb7fc1f88687f47ca20bb7227981b03afaae69287029da08096853b738"},
|
||||
{file = "cryptography-46.0.6-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2b417edbe8877cda9022dde3a008e2deb50be9c407eef034aeeb3a8b11d9db3c"},
|
||||
{file = "cryptography-46.0.6-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:380343e0653b1c9d7e1f55b52aaa2dbb2fdf2730088d48c43ca1c7c0abb7cc2f"},
|
||||
{file = "cryptography-46.0.6-cp311-abi3-win32.whl", hash = "sha256:bcb87663e1f7b075e48c3be3ecb5f0b46c8fc50b50a97cf264e7f60242dca3f2"},
|
||||
{file = "cryptography-46.0.6-cp311-abi3-win_amd64.whl", hash = "sha256:6739d56300662c468fddb0e5e291f9b4d084bead381667b9e654c7dd81705124"},
|
||||
{file = "cryptography-46.0.6-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:2ef9e69886cbb137c2aef9772c2e7138dc581fad4fcbcf13cc181eb5a3ab6275"},
|
||||
{file = "cryptography-46.0.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7f417f034f91dcec1cb6c5c35b07cdbb2ef262557f701b4ecd803ee8cefed4f4"},
|
||||
{file = "cryptography-46.0.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d24c13369e856b94892a89ddf70b332e0b70ad4a5c43cf3e9cb71d6d7ffa1f7b"},
|
||||
{file = "cryptography-46.0.6-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:aad75154a7ac9039936d50cf431719a2f8d4ed3d3c277ac03f3339ded1a5e707"},
|
||||
{file = "cryptography-46.0.6-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:3c21d92ed15e9cfc6eb64c1f5a0326db22ca9c2566ca46d845119b45b4400361"},
|
||||
{file = "cryptography-46.0.6-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:4668298aef7cddeaf5c6ecc244c2302a2b8e40f384255505c22875eebb47888b"},
|
||||
{file = "cryptography-46.0.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:8ce35b77aaf02f3b59c90b2c8a05c73bac12cea5b4e8f3fbece1f5fddea5f0ca"},
|
||||
{file = "cryptography-46.0.6-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:c89eb37fae9216985d8734c1afd172ba4927f5a05cfd9bf0e4863c6d5465b013"},
|
||||
{file = "cryptography-46.0.6-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:ed418c37d095aeddf5336898a132fba01091f0ac5844e3e8018506f014b6d2c4"},
|
||||
{file = "cryptography-46.0.6-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:69cf0056d6947edc6e6760e5f17afe4bea06b56a9ac8a06de9d2bd6b532d4f3a"},
|
||||
{file = "cryptography-46.0.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e7304c4f4e9490e11efe56af6713983460ee0780f16c63f219984dab3af9d2d"},
|
||||
{file = "cryptography-46.0.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b928a3ca837c77a10e81a814a693f2295200adb3352395fad024559b7be7a736"},
|
||||
{file = "cryptography-46.0.6-cp314-cp314t-win32.whl", hash = "sha256:97c8115b27e19e592a05c45d0dd89c57f81f841cc9880e353e0d3bf25b2139ed"},
|
||||
{file = "cryptography-46.0.6-cp314-cp314t-win_amd64.whl", hash = "sha256:c797e2517cb7880f8297e2c0f43bb910e91381339336f75d2c1c2cbf811b70b4"},
|
||||
{file = "cryptography-46.0.6-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:12cae594e9473bca1a7aceb90536060643128bb274fcea0fc459ab90f7d1ae7a"},
|
||||
{file = "cryptography-46.0.6-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:639301950939d844a9e1c4464d7e07f902fe9a7f6b215bb0d4f28584729935d8"},
|
||||
{file = "cryptography-46.0.6-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ed3775295fb91f70b4027aeba878d79b3e55c0b3e97eaa4de71f8f23a9f2eb77"},
|
||||
{file = "cryptography-46.0.6-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8927ccfbe967c7df312ade694f987e7e9e22b2425976ddbf28271d7e58845290"},
|
||||
{file = "cryptography-46.0.6-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:b12c6b1e1651e42ab5de8b1e00dc3b6354fdfd778e7fa60541ddacc27cd21410"},
|
||||
{file = "cryptography-46.0.6-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:063b67749f338ca9c5a0b7fe438a52c25f9526b851e24e6c9310e7195aad3b4d"},
|
||||
{file = "cryptography-46.0.6-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:02fad249cb0e090b574e30b276a3da6a149e04ee2f049725b1f69e7b8351ec70"},
|
||||
{file = "cryptography-46.0.6-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e6142674f2a9291463e5e150090b95a8519b2fb6e6aaec8917dd8d094ce750d"},
|
||||
{file = "cryptography-46.0.6-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:456b3215172aeefb9284550b162801d62f5f264a081049a3e94307fe20792cfa"},
|
||||
{file = "cryptography-46.0.6-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:341359d6c9e68834e204ceaf25936dffeafea3829ab80e9503860dcc4f4dac58"},
|
||||
{file = "cryptography-46.0.6-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9a9c42a2723999a710445bc0d974e345c32adfd8d2fac6d8a251fa829ad31cfb"},
|
||||
{file = "cryptography-46.0.6-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6617f67b1606dfd9fe4dbfa354a9508d4a6d37afe30306fe6c101b7ce3274b72"},
|
||||
{file = "cryptography-46.0.6-cp38-abi3-win32.whl", hash = "sha256:7f6690b6c55e9c5332c0b59b9c8a3fb232ebf059094c17f9019a51e9827df91c"},
|
||||
{file = "cryptography-46.0.6-cp38-abi3-win_amd64.whl", hash = "sha256:79e865c642cfc5c0b3eb12af83c35c5aeff4fa5c672dc28c43721c2c9fdd2f0f"},
|
||||
{file = "cryptography-46.0.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:2ea0f37e9a9cf0df2952893ad145fd9627d326a59daec9b0802480fa3bcd2ead"},
|
||||
{file = "cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a3e84d5ec9ba01f8fd03802b2147ba77f0c8f2617b2aff254cedd551844209c8"},
|
||||
{file = "cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:12f0fa16cc247b13c43d56d7b35287ff1569b5b1f4c5e87e92cc4fcc00cd10c0"},
|
||||
{file = "cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:50575a76e2951fe7dbd1f56d181f8c5ceeeb075e9ff88e7ad997d2f42af06e7b"},
|
||||
{file = "cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:90e5f0a7b3be5f40c3a0a0eafb32c681d8d2c181fc2a1bdabe9b3f611d9f6b1a"},
|
||||
{file = "cryptography-46.0.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6728c49e3b2c180ef26f8e9f0a883a2c585638db64cf265b49c9ba10652d430e"},
|
||||
{file = "cryptography-46.0.6.tar.gz", hash = "sha256:27550628a518c5c6c903d84f637fbecf287f6cb9ced3804838a1295dc1fd0759"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""}
|
||||
cffi = {version = ">=2.0.0", markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\""}
|
||||
|
||||
[package.extras]
|
||||
docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=3.0.0) ; python_version >= \"3.8\""]
|
||||
docs = ["sphinx (>=5.3.0)", "sphinx-inline-tabs", "sphinx-rtd-theme (>=3.0.0)"]
|
||||
docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"]
|
||||
nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2) ; python_version >= \"3.8\""]
|
||||
pep8test = ["check-sdist ; python_version >= \"3.8\"", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"]
|
||||
nox = ["nox[uv] (>=2024.4.15)"]
|
||||
pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.14)", "ruff (>=0.11.11)"]
|
||||
sdist = ["build (>=1.0.0)"]
|
||||
ssh = ["bcrypt (>=3.1.5)"]
|
||||
test = ["certifi (>=2024)", "cryptography-vectors (==44.0.3)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"]
|
||||
test = ["certifi (>=2024)", "cryptography-vectors (==46.0.6)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"]
|
||||
test-randomorder = ["pytest-randomly"]
|
||||
|
||||
[[package]]
|
||||
@@ -3740,19 +3752,19 @@ urllib3 = ["packaging", "urllib3"]
|
||||
|
||||
[[package]]
|
||||
name = "google-auth-httplib2"
|
||||
version = "0.2.1"
|
||||
version = "0.2.0"
|
||||
description = "Google Authentication Library: httplib2 transport"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
python-versions = "*"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "google_auth_httplib2-0.2.1-py3-none-any.whl", hash = "sha256:1be94c611db91c01f9703e7f62b0a59bbd5587a95571c7b6fade510d648bc08b"},
|
||||
{file = "google_auth_httplib2-0.2.1.tar.gz", hash = "sha256:5ef03be3927423c87fb69607b42df23a444e434ddb2555b73b3679793187b7de"},
|
||||
{file = "google-auth-httplib2-0.2.0.tar.gz", hash = "sha256:38aa7badf48f974f1eb9861794e9c0cb2a0511a4ec0679b1f886d108f5640e05"},
|
||||
{file = "google_auth_httplib2-0.2.0-py2.py3-none-any.whl", hash = "sha256:b65a0a2123300dd71281a7bf6e64d65a0759287df52729bdd1ae2e47dc311a3d"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
google-auth = ">=1.32.0,<3.0.0"
|
||||
httplib2 = ">=0.19.0,<1.0.0"
|
||||
google-auth = "*"
|
||||
httplib2 = ">=0.19.0"
|
||||
|
||||
[[package]]
|
||||
name = "google-cloud-access-context-manager"
|
||||
@@ -5925,23 +5937,24 @@ signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"]
|
||||
|
||||
[[package]]
|
||||
name = "oci"
|
||||
version = "2.160.3"
|
||||
version = "2.169.0"
|
||||
description = "Oracle Cloud Infrastructure Python SDK"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "oci-2.160.3-py3-none-any.whl", hash = "sha256:858bff3e697098bdda44833d2476bfb4632126f0182178e7dbde4dbd156d71f0"},
|
||||
{file = "oci-2.160.3.tar.gz", hash = "sha256:57514889be3b713a8385d86e3ba8a33cf46e3563c2a7e29a93027fb30b8a2537"},
|
||||
{file = "oci-2.169.0-py3-none-any.whl", hash = "sha256:c71bb5143f307791082b3e33cc1545c2490a518cfed85ab1948ef5107c36d30b"},
|
||||
{file = "oci-2.169.0.tar.gz", hash = "sha256:f3c5fff00b01783b5325ea7b13bf140053ec1e9f41da20bfb9c8a349ee7662fa"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
certifi = "*"
|
||||
circuitbreaker = {version = ">=1.3.1,<3.0.0", markers = "python_version >= \"3.7\""}
|
||||
cryptography = ">=3.2.1,<46.0.0"
|
||||
pyOpenSSL = ">=17.5.0,<25.0.0"
|
||||
cryptography = ">=3.2.1,<47.0.0"
|
||||
pyOpenSSL = ">=17.5.0,<27.0.0"
|
||||
python-dateutil = ">=2.5.3,<3.0.0"
|
||||
pytz = ">=2016.10"
|
||||
urllib3 = {version = ">=2.6.3", markers = "python_version >= \"3.10.0\""}
|
||||
|
||||
[package.extras]
|
||||
adk = ["docstring-parser (>=0.16) ; python_version >= \"3.10\" and python_version < \"4\"", "mcp (>=1.6.0) ; python_version >= \"3.10\" and python_version < \"4\"", "pydantic (>=2.10.6) ; python_version >= \"3.10\" and python_version < \"4\"", "rich (>=13.9.4) ; python_version >= \"3.10\" and python_version < \"4\""]
|
||||
@@ -6659,7 +6672,7 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "prowler"
|
||||
version = "5.23.0"
|
||||
version = "5.24.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.10,<3.13"
|
||||
@@ -6679,7 +6692,7 @@ alibabacloud-rds20140815 = "12.0.0"
|
||||
alibabacloud_sas20181203 = "6.1.0"
|
||||
alibabacloud-sls20201230 = "5.9.0"
|
||||
alibabacloud_sts20150401 = "1.1.6"
|
||||
alibabacloud_tea_openapi = "0.4.1"
|
||||
alibabacloud_tea_openapi = "0.4.4"
|
||||
alibabacloud_vpc20160428 = "6.13.0"
|
||||
alive-progress = "3.3.0"
|
||||
awsipranges = "0.3.3"
|
||||
@@ -6714,14 +6727,14 @@ boto3 = "1.40.61"
|
||||
botocore = "1.40.61"
|
||||
cloudflare = "4.3.1"
|
||||
colorama = "0.4.6"
|
||||
cryptography = "44.0.3"
|
||||
cryptography = "46.0.6"
|
||||
dash = "3.1.1"
|
||||
dash-bootstrap-components = "2.0.3"
|
||||
defusedxml = ">=0.7.1"
|
||||
defusedxml = "0.7.1"
|
||||
detect-secrets = "1.5.0"
|
||||
dulwich = "0.23.0"
|
||||
google-api-python-client = "2.163.0"
|
||||
google-auth-httplib2 = ">=0.1,<0.3"
|
||||
google-auth-httplib2 = "0.2.0"
|
||||
h2 = "4.3.0"
|
||||
jsonschema = "4.23.0"
|
||||
kubernetes = "32.0.1"
|
||||
@@ -6729,14 +6742,14 @@ markdown = "3.10.2"
|
||||
microsoft-kiota-abstractions = "1.9.2"
|
||||
msgraph-sdk = "1.23.0"
|
||||
numpy = "2.0.2"
|
||||
oci = "2.160.3"
|
||||
oci = "2.169.0"
|
||||
openstacksdk = "4.2.0"
|
||||
pandas = "2.2.3"
|
||||
py-iam-expand = "0.1.0"
|
||||
py-ocsf-models = "0.8.1"
|
||||
pydantic = ">=2.0,<3.0"
|
||||
pydantic = "2.12.5"
|
||||
pygithub = "2.8.0"
|
||||
python-dateutil = ">=2.9.0.post0,<3.0.0"
|
||||
python-dateutil = "2.9.0.post0"
|
||||
pytz = "2025.1"
|
||||
schema = "0.7.5"
|
||||
shodan = "1.31.0"
|
||||
@@ -6748,8 +6761,8 @@ uuid6 = "2024.7.10"
|
||||
[package.source]
|
||||
type = "git"
|
||||
url = "https://github.com/prowler-cloud/prowler.git"
|
||||
reference = "master"
|
||||
resolved_reference = "6ac90eb1b58590b6f2f51645dbef17b9231053f4"
|
||||
reference = "v5.24"
|
||||
resolved_reference = "ba5b23245f4805f46d67e67fc059aefd6831f7b3"
|
||||
|
||||
[[package]]
|
||||
name = "psutil"
|
||||
@@ -6958,11 +6971,11 @@ description = "C parser in Python"
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
groups = ["main", "dev"]
|
||||
markers = "platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\""
|
||||
files = [
|
||||
{file = "pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992"},
|
||||
{file = "pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29"},
|
||||
]
|
||||
markers = {main = "implementation_name != \"PyPy\" and platform_python_implementation != \"PyPy\"", dev = "platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\""}
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
@@ -7288,18 +7301,19 @@ tests = ["hypothesis (>=3.27.0)", "pytest (>=7.4.0)", "pytest-cov (>=2.10.1)", "
|
||||
|
||||
[[package]]
|
||||
name = "pyopenssl"
|
||||
version = "24.3.0"
|
||||
version = "26.0.0"
|
||||
description = "Python wrapper module around the OpenSSL library"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "pyOpenSSL-24.3.0-py3-none-any.whl", hash = "sha256:e474f5a473cd7f92221cc04976e48f4d11502804657a08a989fb3be5514c904a"},
|
||||
{file = "pyopenssl-24.3.0.tar.gz", hash = "sha256:49f7a019577d834746bc55c5fce6ecbcec0f2b4ec5ce1cf43a9a173b8138bb36"},
|
||||
{file = "pyopenssl-26.0.0-py3-none-any.whl", hash = "sha256:df94d28498848b98cc1c0ffb8ef1e71e40210d3b0a8064c9d29571ed2904bf81"},
|
||||
{file = "pyopenssl-26.0.0.tar.gz", hash = "sha256:f293934e52936f2e3413b89c6ce36df66a0b34ae1ea3a053b8c5020ff2f513fc"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
cryptography = ">=41.0.5,<45"
|
||||
cryptography = ">=46.0.0,<47"
|
||||
typing-extensions = {version = ">=4.9", markers = "python_version < \"3.13\" and python_version >= \"3.8\""}
|
||||
|
||||
[package.extras]
|
||||
docs = ["sphinx (!=5.2.0,!=5.2.0.post0,!=7.2.5)", "sphinx_rtd_theme"]
|
||||
@@ -9400,4 +9414,4 @@ files = [
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = ">=3.11,<3.13"
|
||||
content-hash = "077e89853cfe3a6d934841488cfa5a98ff6c92b71f74b817b71387d11559f143"
|
||||
content-hash = "44caea5e040c54d4c726144d644e67c942b5acf7e316b1fcb2c22b0947614fbe"
|
||||
|
||||
@@ -25,7 +25,7 @@ dependencies = [
|
||||
"defusedxml==0.7.1",
|
||||
"gunicorn==23.0.0",
|
||||
"lxml==5.3.2",
|
||||
"prowler @ git+https://github.com/prowler-cloud/prowler.git@master",
|
||||
"prowler @ git+https://github.com/prowler-cloud/prowler.git@v5.24",
|
||||
"psycopg2-binary==2.9.9",
|
||||
"pytest-celery[redis] (==1.3.0)",
|
||||
"sentry-sdk[django] (==2.56.0)",
|
||||
|
||||
@@ -33,6 +33,41 @@ To scan a particular AWS region with Prowler, use:
|
||||
prowler aws -f/--region eu-west-1 us-east-1
|
||||
```
|
||||
|
||||
### Excluding Specific Regions
|
||||
|
||||
To scan all supported AWS regions except a specific subset, use the `--excluded-region` flag:
|
||||
|
||||
```console
|
||||
prowler aws --excluded-region eu-west-1 me-south-1
|
||||
```
|
||||
|
||||
You can also configure the exclusion list with the `PROWLER_AWS_DISALLOWED_REGIONS` environment variable as a comma-separated list:
|
||||
|
||||
```console
|
||||
export PROWLER_AWS_DISALLOWED_REGIONS="eu-west-1,me-south-1"
|
||||
prowler aws
|
||||
```
|
||||
|
||||
Or with the AWS provider configuration in `config.yaml`:
|
||||
|
||||
```yaml
|
||||
aws:
|
||||
disallowed_regions:
|
||||
- eu-west-1
|
||||
- me-south-1
|
||||
```
|
||||
|
||||
When more than one source is set, precedence is:
|
||||
|
||||
1. `--excluded-region`
|
||||
2. `PROWLER_AWS_DISALLOWED_REGIONS`
|
||||
3. `aws.disallowed_regions` in `config.yaml`
|
||||
|
||||
<Note>
|
||||
For self-hosted App or API-triggered scans, set `PROWLER_AWS_DISALLOWED_REGIONS` in the runtime environment of the backend scan containers such as `api` and `worker`. The `ui` container does not enforce AWS region selection.
|
||||
|
||||
</Note>
|
||||
|
||||
### AWS Credentials Configuration
|
||||
|
||||
For details on configuring AWS credentials, refer to the following [Botocore](https://github.com/boto/botocore) [file](https://github.com/boto/botocore/blob/22a19ea7c4c2c4dd7df4ab8c32733cba0c7597a4/botocore/data/partitions.json).
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
All notable changes to the **Prowler SDK** are documented in this file.
|
||||
|
||||
## [5.24.0] (Prowler UNRELEASED)
|
||||
## [5.24.0] (Prowler v5.24.0)
|
||||
|
||||
### 🚀 Added
|
||||
|
||||
@@ -13,10 +13,11 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
- `iam_role_access_not_stale_to_bedrock` and `iam_user_access_not_stale_to_bedrock` checks for AWS provider [(#10536)](https://github.com/prowler-cloud/prowler/pull/10536)
|
||||
- `iam_policy_no_wildcard_marketplace_subscribe` and `iam_inline_policy_no_wildcard_marketplace_subscribe` checks for AWS provider [(#10525)](https://github.com/prowler-cloud/prowler/pull/10525)
|
||||
- `bedrock_vpc_endpoints_configured` check for AWS provider [(#10591)](https://github.com/prowler-cloud/prowler/pull/10591)
|
||||
- `exchange_organization_delicensing_resiliency_enabled` check for m365 provider [(#10608)](https://github.com/prowler-cloud/prowler/pull/10608)
|
||||
- `exchange_organization_delicensing_resiliency_enabled` check for M365 provider [(#10608)](https://github.com/prowler-cloud/prowler/pull/10608)
|
||||
- `entra_conditional_access_policy_mfa_enforced_for_guest_users` check for M365 provider [(#10616)](https://github.com/prowler-cloud/prowler/pull/10616)
|
||||
- `entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced` check for m365 provider [(#10618)](https://github.com/prowler-cloud/prowler/pull/10618)
|
||||
- `entra_conditional_access_policy_block_unknown_device_platforms` check for m365 provider [(#10615)](https://github.com/prowler-cloud/prowler/pull/10615)
|
||||
- `entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced` check for M365 provider [(#10618)](https://github.com/prowler-cloud/prowler/pull/10618)
|
||||
- `entra_conditional_access_policy_block_unknown_device_platforms` check for M365 provider [(#10615)](https://github.com/prowler-cloud/prowler/pull/10615)
|
||||
- `--excluded-region` CLI flag, `PROWLER_AWS_DISALLOWED_REGIONS` environment variable, and `aws.disallowed_regions` config entry to skip specific AWS regions during scans [(#10688)](https://github.com/prowler-cloud/prowler/pull/10688)
|
||||
|
||||
### 🔄 Changed
|
||||
|
||||
@@ -24,6 +25,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
- Normalize Conditional Access platform values in Entra models and simplify platform-based checks [(#10635)](https://github.com/prowler-cloud/prowler/pull/10635)
|
||||
|
||||
### 🐞 Fixed
|
||||
|
||||
- Vercel firewall config handling for team-scoped projects and current API response shapes [(#10695)](https://github.com/prowler-cloud/prowler/pull/10695)
|
||||
|
||||
---
|
||||
@@ -790,7 +792,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
- S3 `test_connection` uses AWS S3 API `HeadBucket` instead of `GetBucketLocation` [(#8456)](https://github.com/prowler-cloud/prowler/pull/8456)
|
||||
- Add more validations to Azure Storage models when some values are None to avoid serialization issues [(#8325)](https://github.com/prowler-cloud/prowler/pull/8325)
|
||||
- `sns_topics_not_publicly_accessible` false positive with `aws:SourceArn` conditions [(#8326)](https://github.com/prowler-cloud/prowler/issues/8326)
|
||||
- Remove typo from description req 1.2.3 - Prowler ThreatScore m365 [(#8384)](https://github.com/prowler-cloud/prowler/pull/8384)
|
||||
- Remove typo from description req 1.2.3 - Prowler ThreatScore M365 [(#8384)](https://github.com/prowler-cloud/prowler/pull/8384)
|
||||
- Way of counting FAILED/PASS reqs from `kisa_isms_p_2023_aws` table [(#8382)](https://github.com/prowler-cloud/prowler/pull/8382)
|
||||
- Use default tenant domain instead of first domain in list for Azure and M365 providers [(#8402)](https://github.com/prowler-cloud/prowler/pull/8402)
|
||||
- Avoid multiple module error calls in M365 provider [(#8353)](https://github.com/prowler-cloud/prowler/pull/8353)
|
||||
@@ -831,7 +833,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
|
||||
- Title & description wording for `iam_user_accesskey_unused` check for AWS provider [(#8233)](https://github.com/prowler-cloud/prowler/pull/8233)
|
||||
- Add GitHub provider to lateral panel in documentation and change -h environment variable output [(#8246)](https://github.com/prowler-cloud/prowler/pull/8246)
|
||||
- Show `m365_identity_type` and `m365_identity_id` in cloud reports [(#8247)](https://github.com/prowler-cloud/prowler/pull/8247)
|
||||
- Show `M365_identity_type` and `M365_identity_id` in cloud reports [(#8247)](https://github.com/prowler-cloud/prowler/pull/8247)
|
||||
- Ensure `is_service_role` only returns `True` for service roles [(#8274)](https://github.com/prowler-cloud/prowler/pull/8274)
|
||||
- Update DynamoDB check metadata to fix broken link [(#8273)](https://github.com/prowler-cloud/prowler/pull/8273)
|
||||
- Show correct count of findings in Dashboard Security Posture page [(#8270)](https://github.com/prowler-cloud/prowler/pull/8270)
|
||||
@@ -953,9 +955,9 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
|
||||
### Fixed
|
||||
|
||||
- `m365_powershell test_credentials` to use sanitized credentials [(#7761)](https://github.com/prowler-cloud/prowler/pull/7761)
|
||||
- `M365_powershell test_credentials` to use sanitized credentials [(#7761)](https://github.com/prowler-cloud/prowler/pull/7761)
|
||||
- `admincenter_users_admins_reduced_license_footprint` check logic to pass when admin user has no license [(#7779)](https://github.com/prowler-cloud/prowler/pull/7779)
|
||||
- `m365_powershell` to close the PowerShell sessions in msgraph services [(#7816)](https://github.com/prowler-cloud/prowler/pull/7816)
|
||||
- `M365_powershell` to close the PowerShell sessions in msgraph services [(#7816)](https://github.com/prowler-cloud/prowler/pull/7816)
|
||||
- `defender_ensure_notify_alerts_severity_is_high`check to accept high or lower severity [(#7862)](https://github.com/prowler-cloud/prowler/pull/7862)
|
||||
- Replace `Directory.Read.All` permission with `Domain.Read.All` which is more restrictive [(#7888)](https://github.com/prowler-cloud/prowler/pull/7888)
|
||||
- Split calls to list Azure Functions attributes [(#7778)](https://github.com/prowler-cloud/prowler/pull/7778)
|
||||
@@ -1029,7 +1031,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
- New check `teams_meeting_chat_anonymous_users_disabled` [(#7579)](https://github.com/prowler-cloud/prowler/pull/7579)
|
||||
- Prowler Threat Score Compliance Framework [(#7603)](https://github.com/prowler-cloud/prowler/pull/7603)
|
||||
- Documentation for M365 provider [(#7622)](https://github.com/prowler-cloud/prowler/pull/7622)
|
||||
- Support for m365 provider in Prowler Dashboard [(#7633)](https://github.com/prowler-cloud/prowler/pull/7633)
|
||||
- Support for M365 provider in Prowler Dashboard [(#7633)](https://github.com/prowler-cloud/prowler/pull/7633)
|
||||
- New check for Modern Authentication enabled for Exchange Online in M365 [(#7636)](https://github.com/prowler-cloud/prowler/pull/7636)
|
||||
- New check `sharepoint_onedrive_sync_restricted_unmanaged_devices` [(#7589)](https://github.com/prowler-cloud/prowler/pull/7589)
|
||||
- New check for Additional Storage restricted for Exchange in M365 [(#7638)](https://github.com/prowler-cloud/prowler/pull/7638)
|
||||
@@ -1039,7 +1041,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
- New check for MailTips full enabled for Exchange in M365 [(#7637)](https://github.com/prowler-cloud/prowler/pull/7637)
|
||||
- New check for Comprehensive Attachments Filter Applied for Defender in M365 [(#7661)](https://github.com/prowler-cloud/prowler/pull/7661)
|
||||
- Modified check `exchange_mailbox_properties_auditing_enabled` to make it configurable [(#7662)](https://github.com/prowler-cloud/prowler/pull/7662)
|
||||
- snapshots to m365 documentation [(#7673)](https://github.com/prowler-cloud/prowler/pull/7673)
|
||||
- snapshots to M365 documentation [(#7673)](https://github.com/prowler-cloud/prowler/pull/7673)
|
||||
- support for static credentials for sending findings to Amazon S3 and AWS Security Hub [(#7322)](https://github.com/prowler-cloud/prowler/pull/7322)
|
||||
- Prowler ThreatScore for M365 provider [(#7692)](https://github.com/prowler-cloud/prowler/pull/7692)
|
||||
- Microsoft User and User Credential auth to reports [(#7681)](https://github.com/prowler-cloud/prowler/pull/7681)
|
||||
|
||||
@@ -69,11 +69,11 @@ from prowler.lib.outputs.compliance.cis.cis_gcp import GCPCIS
|
||||
from prowler.lib.outputs.compliance.cis.cis_github import GithubCIS
|
||||
from prowler.lib.outputs.compliance.cis.cis_googleworkspace import GoogleWorkspaceCIS
|
||||
from prowler.lib.outputs.compliance.cis.cis_kubernetes import KubernetesCIS
|
||||
from prowler.lib.outputs.compliance.cis.cis_m365 import M365CIS
|
||||
from prowler.lib.outputs.compliance.cis.cis_oraclecloud import OracleCloudCIS
|
||||
from prowler.lib.outputs.compliance.cisa_scuba.cisa_scuba_googleworkspace import (
|
||||
GoogleWorkspaceCISASCuBA,
|
||||
)
|
||||
from prowler.lib.outputs.compliance.cis.cis_m365 import M365CIS
|
||||
from prowler.lib.outputs.compliance.cis.cis_oraclecloud import OracleCloudCIS
|
||||
from prowler.lib.outputs.compliance.compliance import display_compliance_table
|
||||
from prowler.lib.outputs.compliance.csa.csa_alibabacloud import AlibabaCloudCSA
|
||||
from prowler.lib.outputs.compliance.csa.csa_aws import AWSCSA
|
||||
@@ -1311,8 +1311,12 @@ def prowler():
|
||||
global_provider.identity.audited_regions,
|
||||
)
|
||||
if not global_provider.identity.audited_regions
|
||||
else global_provider.identity.audited_regions
|
||||
else set(global_provider.identity.audited_regions)
|
||||
)
|
||||
if global_provider._enabled_regions is not None:
|
||||
security_hub_regions = security_hub_regions.intersection(
|
||||
global_provider._enabled_regions
|
||||
)
|
||||
|
||||
security_hub = SecurityHub(
|
||||
aws_account_id=global_provider.identity.account,
|
||||
|
||||
@@ -3,6 +3,10 @@ aws:
|
||||
# AWS Global Configuration
|
||||
# aws.mute_non_default_regions --> Set to True to muted failed findings in non-default regions for AccessAnalyzer, GuardDuty, SecurityHub, DRS and Config
|
||||
mute_non_default_regions: False
|
||||
# aws.disallowed_regions --> List of AWS regions to exclude from the scan.
|
||||
# Also settable via the PROWLER_AWS_DISALLOWED_REGIONS environment variable or
|
||||
# the --excluded-region CLI flag. Precedence: CLI > env var > config file.
|
||||
# disallowed_regions: []
|
||||
# If you want to mute failed findings only in specific regions, create a file with the following syntax and run it with `prowler aws -w mutelist.yaml`:
|
||||
# Mutelist:
|
||||
# Accounts:
|
||||
|
||||
@@ -111,6 +111,7 @@ class AwsProvider(Provider):
|
||||
mfa: bool = False,
|
||||
profile: str = None,
|
||||
regions: set = set(),
|
||||
excluded_regions: set = None,
|
||||
organizations_role_arn: str = None,
|
||||
scan_unused_services: bool = False,
|
||||
resource_tags: list[str] = [],
|
||||
@@ -136,6 +137,10 @@ class AwsProvider(Provider):
|
||||
- mfa: A boolean indicating whether MFA is enabled.
|
||||
- profile: The name of the AWS CLI profile to use.
|
||||
- regions: A set of regions to audit.
|
||||
- excluded_regions: A set of regions to skip during the scan. Applied
|
||||
on top of `regions` and of the account's enabled regions. Also
|
||||
settable via the PROWLER_AWS_DISALLOWED_REGIONS environment variable
|
||||
or the `disallowed_regions` key in the provider config file.
|
||||
- organizations_role_arn: The ARN of the AWS Organizations IAM role to assume.
|
||||
- scan_unused_services: A boolean indicating whether to scan unused services. False by default.
|
||||
- resource_tags: A list of tags to filter the resources to audit.
|
||||
@@ -190,6 +195,33 @@ class AwsProvider(Provider):
|
||||
|
||||
logger.info("Initializing AWS provider ...")
|
||||
|
||||
# Load provider config early because provider-level settings can affect
|
||||
# bootstrap region selection before the scan starts.
|
||||
if config_content is not None:
|
||||
self._audit_config = config_content
|
||||
else:
|
||||
if not config_path:
|
||||
config_path = default_config_file_path
|
||||
self._audit_config = load_and_validate_config_file(self._type, config_path)
|
||||
|
||||
excluded_regions = self.resolve_excluded_regions(
|
||||
excluded_regions, self._audit_config
|
||||
)
|
||||
|
||||
# Normalize excluded_regions and prune the include-list up front so
|
||||
# every downstream consumer (identity, STS region, service/region
|
||||
# enumeration) sees an already-filtered view.
|
||||
if excluded_regions and regions:
|
||||
regions = set(regions) - excluded_regions
|
||||
if not regions:
|
||||
raise AWSArgumentTypeValidationError(
|
||||
message=(
|
||||
"All requested AWS regions are excluded by the "
|
||||
"disallowed regions configuration."
|
||||
),
|
||||
file=pathlib.Path(__file__).name,
|
||||
)
|
||||
|
||||
######## AWS Session
|
||||
logger.info("Generating original session ...")
|
||||
|
||||
@@ -215,7 +247,7 @@ class AwsProvider(Provider):
|
||||
# After the session is created, validate it
|
||||
logger.info("Validating credentials ...")
|
||||
sts_region = get_aws_region_for_sts(
|
||||
self.session.current_session.region_name, regions
|
||||
self.session.current_session.region_name, regions, excluded_regions
|
||||
)
|
||||
|
||||
# Validate the credentials
|
||||
@@ -229,7 +261,9 @@ class AwsProvider(Provider):
|
||||
|
||||
######## AWS Provider Identity
|
||||
# Get profile region
|
||||
profile_region = self.get_profile_region(self._session.current_session)
|
||||
profile_region = self.get_profile_region(
|
||||
self._session.current_session, excluded_regions
|
||||
)
|
||||
|
||||
# Set identity
|
||||
self._identity = self.set_identity(
|
||||
@@ -332,7 +366,26 @@ class AwsProvider(Provider):
|
||||
)
|
||||
########
|
||||
|
||||
# Parse Scan Tags
|
||||
# Get Enabled Regions
|
||||
self._enabled_regions = self.get_aws_enabled_regions(
|
||||
self._session.current_session
|
||||
)
|
||||
|
||||
# Apply the exclusion to the account's enabled regions. This is the
|
||||
# gate used by generate_regional_clients, so skipped regions never get
|
||||
# a boto3 client created for them and cannot stall the scan.
|
||||
if excluded_regions:
|
||||
if self._enabled_regions is not None:
|
||||
self._enabled_regions = self._enabled_regions - excluded_regions
|
||||
if self._identity.audited_regions:
|
||||
self._identity.audited_regions = (
|
||||
set(self._identity.audited_regions) - excluded_regions
|
||||
)
|
||||
logger.info(f"Excluding AWS regions from scan: {sorted(excluded_regions)}")
|
||||
self._excluded_regions = excluded_regions
|
||||
|
||||
# Parse Scan Tags after region exclusions are applied so tag discovery
|
||||
# also skips disallowed regions.
|
||||
if resource_tags:
|
||||
self._audit_resources = self.get_tagged_resources(resource_tags)
|
||||
|
||||
@@ -340,22 +393,9 @@ class AwsProvider(Provider):
|
||||
if resource_arn:
|
||||
self._audit_resources = resource_arn
|
||||
|
||||
# Get Enabled Regions
|
||||
self._enabled_regions = self.get_aws_enabled_regions(
|
||||
self._session.current_session
|
||||
)
|
||||
|
||||
# Set ignore unused services
|
||||
self._scan_unused_services = scan_unused_services
|
||||
|
||||
# Audit Config
|
||||
if config_content:
|
||||
self._audit_config = config_content
|
||||
else:
|
||||
if not config_path:
|
||||
config_path = default_config_file_path
|
||||
self._audit_config = load_and_validate_config_file(self._type, config_path)
|
||||
|
||||
# Fixer Config
|
||||
self._fixer_config = fixer_config
|
||||
|
||||
@@ -468,12 +508,53 @@ class AwsProvider(Provider):
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_profile_region(session: Session):
|
||||
profile_region = AWS_REGION_US_EAST_1
|
||||
if session.region_name:
|
||||
profile_region = session.region_name
|
||||
def resolve_excluded_regions(
|
||||
excluded_regions: set | list | tuple | None,
|
||||
audit_config: dict | None,
|
||||
) -> set[str]:
|
||||
"""Resolve AWS region exclusions with precedence arg > env > config."""
|
||||
if excluded_regions is not None:
|
||||
raw_regions = excluded_regions
|
||||
else:
|
||||
raw_regions = Provider.get_excluded_regions_from_env()
|
||||
if not raw_regions and isinstance(audit_config, dict):
|
||||
raw_regions = audit_config.get("disallowed_regions") or []
|
||||
|
||||
return profile_region
|
||||
return {str(region).strip() for region in raw_regions if str(region).strip()}
|
||||
|
||||
@staticmethod
|
||||
def get_bootstrap_region_candidates(session_region: str | None) -> tuple[str, ...]:
|
||||
"""Return safe fallback regions for bootstrap AWS calls."""
|
||||
if session_region:
|
||||
if session_region.startswith("cn-"):
|
||||
return ("cn-north-1", "cn-northwest-1")
|
||||
if session_region.startswith("us-gov-"):
|
||||
return ("us-gov-east-1", "us-gov-west-1")
|
||||
if session_region.startswith("eusc-"):
|
||||
return ("eusc-de-east-1",)
|
||||
if session_region.startswith("us-iso"):
|
||||
return (session_region,)
|
||||
|
||||
return (AWS_STS_GLOBAL_ENDPOINT_REGION, "us-east-2", "us-west-2", "eu-west-1")
|
||||
|
||||
@staticmethod
|
||||
def get_profile_region(
|
||||
session: Session, excluded_regions: set[str] | None = None
|
||||
) -> str:
|
||||
excluded_regions = set(excluded_regions or ())
|
||||
session_region = session.region_name
|
||||
if session_region and session_region not in excluded_regions:
|
||||
return session_region
|
||||
|
||||
for region in AwsProvider.get_bootstrap_region_candidates(session_region):
|
||||
if region not in excluded_regions:
|
||||
if session_region and session_region != region:
|
||||
logger.info(
|
||||
f"Configured AWS profile region {session_region} is excluded; using {region} for bootstrap clients."
|
||||
)
|
||||
return region
|
||||
|
||||
return session_region or AWS_REGION_US_EAST_1
|
||||
|
||||
@staticmethod
|
||||
def set_identity(
|
||||
@@ -701,12 +782,15 @@ class AwsProvider(Provider):
|
||||
Caller Identity ARN: arn:aws:iam::123456789012:user/prowler
|
||||
```
|
||||
"""
|
||||
# Beautify audited regions, set "all" if there is no filter region
|
||||
regions = (
|
||||
", ".join(self._identity.audited_regions)
|
||||
if self._identity.audited_regions is not None
|
||||
else "all"
|
||||
)
|
||||
# Beautify audited regions. If the scan includes all regions but some
|
||||
# are explicitly excluded, reflect that in the banner instead of
|
||||
# showing the misleading "all" label.
|
||||
if self._identity.audited_regions:
|
||||
regions = ", ".join(sorted(self._identity.audited_regions))
|
||||
elif getattr(self, "_excluded_regions", None):
|
||||
regions = f"all except {', '.join(sorted(self._excluded_regions))}"
|
||||
else:
|
||||
regions = "all"
|
||||
# Beautify audited profile, set "default" if there is no profile set
|
||||
profile = (
|
||||
self._identity.profile if self._identity.profile is not None else "default"
|
||||
@@ -745,6 +829,8 @@ class AwsProvider(Provider):
|
||||
service_regions = AwsProvider.get_available_aws_service_regions(
|
||||
service, self._identity.partition, self._identity.audited_regions
|
||||
)
|
||||
if getattr(self, "_excluded_regions", None):
|
||||
service_regions = service_regions - self._excluded_regions
|
||||
|
||||
# Get the regions enabled for the account and get the intersection with the service available regions
|
||||
if self._enabled_regions is not None:
|
||||
@@ -962,6 +1048,8 @@ class AwsProvider(Provider):
|
||||
service_regions = AwsProvider.get_available_aws_service_regions(
|
||||
service, self._identity.partition, self._identity.audited_regions
|
||||
)
|
||||
if getattr(self, "_excluded_regions", None):
|
||||
service_regions = service_regions - self._excluded_regions
|
||||
default_region = self.get_global_region()
|
||||
# global region of the partition when all regions are audited and there is no profile region
|
||||
if self._identity.profile_region in service_regions:
|
||||
@@ -1565,13 +1653,19 @@ def read_aws_regions_file() -> dict:
|
||||
|
||||
|
||||
# TODO: This can be moved to another class since it doesn't need self
|
||||
def get_aws_region_for_sts(session_region: str, regions: set[str]) -> str:
|
||||
def get_aws_region_for_sts(
|
||||
session_region: str,
|
||||
regions: set[str],
|
||||
excluded_regions: set[str] | None = None,
|
||||
) -> str:
|
||||
"""
|
||||
Get the AWS region for the STS Assume Role operation.
|
||||
|
||||
Args:
|
||||
- session_region (str): The region configured in the AWS session.
|
||||
- regions (set[str]): The regions passed with the -f/--region/--filter-region option.
|
||||
- excluded_regions (set[str] | None): Regions that should be avoided for
|
||||
bootstrap calls when possible.
|
||||
|
||||
Returns:
|
||||
str: The AWS region for the STS Assume Role operation
|
||||
@@ -1579,20 +1673,21 @@ def get_aws_region_for_sts(session_region: str, regions: set[str]) -> str:
|
||||
Example:
|
||||
aws_region = get_aws_region_for_sts(session_region, regions)
|
||||
"""
|
||||
# If there is no region passed with -f/--region/--filter-region
|
||||
if regions is None or len(regions) == 0:
|
||||
# If you have a region configured in your AWS config or credentials file
|
||||
if session_region is not None:
|
||||
aws_region = session_region
|
||||
else:
|
||||
# If there is no region set passed with -f/--region
|
||||
# we use the Global STS Endpoint Region, us-east-1
|
||||
aws_region = AWS_STS_GLOBAL_ENDPOINT_REGION
|
||||
else:
|
||||
# Get the first region passed to the -f/--region
|
||||
aws_region = list(regions)[0]
|
||||
excluded_regions = set(excluded_regions or ())
|
||||
|
||||
return aws_region
|
||||
if regions:
|
||||
for region in regions:
|
||||
if region not in excluded_regions:
|
||||
return region
|
||||
|
||||
if session_region and session_region not in excluded_regions:
|
||||
return session_region
|
||||
|
||||
for region in AwsProvider.get_bootstrap_region_candidates(session_region):
|
||||
if region not in excluded_regions:
|
||||
return region
|
||||
|
||||
return session_region or AWS_STS_GLOBAL_ENDPOINT_REGION
|
||||
|
||||
|
||||
# TODO: this duplicates the provider arguments validation library
|
||||
|
||||
@@ -66,6 +66,16 @@ def init_parser(self):
|
||||
help="AWS region names to run Prowler against",
|
||||
choices=AwsProvider.get_regions(partition=None),
|
||||
)
|
||||
aws_regions_subparser.add_argument(
|
||||
"--excluded-region",
|
||||
"--excluded-regions",
|
||||
nargs="+",
|
||||
help=(
|
||||
"AWS region names to exclude from the scan. Overrides the "
|
||||
"PROWLER_AWS_DISALLOWED_REGIONS environment variable when set."
|
||||
),
|
||||
choices=AwsProvider.get_regions(partition=None),
|
||||
)
|
||||
# AWS Organizations
|
||||
aws_orgs_subparser = aws_parser.add_argument_group("AWS Organizations")
|
||||
aws_orgs_subparser.add_argument(
|
||||
|
||||
@@ -30,10 +30,12 @@ def quick_inventory(provider: AwsProvider, args):
|
||||
ec2_client = provider.session.current_session.client(
|
||||
"ec2", region_name=provider.identity.profile_region
|
||||
)
|
||||
excluded_regions = getattr(provider, "_excluded_regions", set())
|
||||
# Get all the available regions
|
||||
provider.identity.audited_regions = [
|
||||
region["RegionName"]
|
||||
for region in ec2_client.describe_regions()["Regions"]
|
||||
if region["RegionName"] not in excluded_regions
|
||||
]
|
||||
|
||||
with alive_bar(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import importlib
|
||||
import os
|
||||
import pkgutil
|
||||
import sys
|
||||
from abc import ABC, abstractmethod
|
||||
@@ -135,6 +136,18 @@ class Provider(ABC):
|
||||
"""
|
||||
return set()
|
||||
|
||||
@staticmethod
|
||||
def get_excluded_regions_from_env() -> set:
|
||||
"""Parse the PROWLER_AWS_DISALLOWED_REGIONS environment variable.
|
||||
|
||||
The variable is a comma-separated list of region identifiers to skip
|
||||
during scans (e.g. "me-south-1, ap-east-1"). Whitespace around entries
|
||||
is tolerated and empty entries are dropped. Returns an empty set when
|
||||
the variable is unset or contains no usable values.
|
||||
"""
|
||||
raw = os.environ.get("PROWLER_AWS_DISALLOWED_REGIONS", "")
|
||||
return {region.strip() for region in raw.split(",") if region.strip()}
|
||||
|
||||
@staticmethod
|
||||
def get_global_provider() -> "Provider":
|
||||
return Provider._global
|
||||
@@ -160,6 +173,11 @@ class Provider(ABC):
|
||||
|
||||
if not isinstance(Provider._global, provider_class):
|
||||
if "aws" in provider_class_name.lower():
|
||||
excluded_regions = (
|
||||
set(arguments.excluded_region)
|
||||
if getattr(arguments, "excluded_region", None)
|
||||
else None
|
||||
)
|
||||
provider_class(
|
||||
retries_max_attempts=arguments.aws_retries_max_attempts,
|
||||
role_arn=arguments.role,
|
||||
@@ -169,6 +187,7 @@ class Provider(ABC):
|
||||
mfa=arguments.mfa,
|
||||
profile=arguments.profile,
|
||||
regions=set(arguments.region) if arguments.region else None,
|
||||
excluded_regions=excluded_regions,
|
||||
organizations_role_arn=arguments.organizations_role,
|
||||
scan_unused_services=arguments.scan_unused_services,
|
||||
resource_tags=arguments.resource_tag,
|
||||
|
||||
@@ -839,6 +839,132 @@ aws:
|
||||
|
||||
assert isinstance(aws_provider, AwsProvider)
|
||||
|
||||
@mock_aws
|
||||
def test_excluded_regions_removed_from_enabled_regions(self):
|
||||
aws_provider = AwsProvider(excluded_regions={AWS_REGION_EU_WEST_1})
|
||||
|
||||
assert AWS_REGION_EU_WEST_1 not in aws_provider._enabled_regions
|
||||
assert AWS_REGION_EU_WEST_1 not in aws_provider.generate_regional_clients("ec2")
|
||||
|
||||
@mock_aws
|
||||
def test_excluded_regions_pruned_from_input_regions(self):
|
||||
aws_provider = AwsProvider(
|
||||
regions={AWS_REGION_EU_WEST_1, AWS_REGION_US_EAST_1},
|
||||
excluded_regions={AWS_REGION_EU_WEST_1},
|
||||
)
|
||||
|
||||
assert AWS_REGION_EU_WEST_1 not in aws_provider._identity.audited_regions
|
||||
assert AWS_REGION_US_EAST_1 in aws_provider._identity.audited_regions
|
||||
|
||||
@mock_aws
|
||||
def test_excluded_regions_from_config_file(self):
|
||||
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as tmp:
|
||||
tmp.write(f"aws:\n disallowed_regions:\n - {AWS_REGION_EU_WEST_1}\n")
|
||||
config_path = tmp.name
|
||||
try:
|
||||
aws_provider = AwsProvider(config_path=config_path)
|
||||
assert AWS_REGION_EU_WEST_1 not in aws_provider._enabled_regions
|
||||
assert aws_provider._excluded_regions == {AWS_REGION_EU_WEST_1}
|
||||
finally:
|
||||
os.remove(config_path)
|
||||
|
||||
@mock_aws
|
||||
def test_excluded_regions_from_env_on_direct_provider_init(self):
|
||||
with mock.patch.dict(
|
||||
os.environ,
|
||||
{"PROWLER_AWS_DISALLOWED_REGIONS": AWS_REGION_EU_WEST_1},
|
||||
clear=False,
|
||||
):
|
||||
aws_provider = AwsProvider()
|
||||
|
||||
assert aws_provider._excluded_regions == {AWS_REGION_EU_WEST_1}
|
||||
assert AWS_REGION_EU_WEST_1 not in aws_provider._enabled_regions
|
||||
|
||||
@mock_aws
|
||||
def test_excluded_regions_precedence_explicit_over_env_and_config(self):
|
||||
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as tmp:
|
||||
tmp.write(f"aws:\n disallowed_regions:\n - {AWS_REGION_EU_WEST_1}\n")
|
||||
config_path = tmp.name
|
||||
try:
|
||||
with mock.patch.dict(
|
||||
os.environ,
|
||||
{"PROWLER_AWS_DISALLOWED_REGIONS": AWS_REGION_US_EAST_1},
|
||||
clear=False,
|
||||
):
|
||||
aws_provider = AwsProvider(
|
||||
config_path=config_path,
|
||||
excluded_regions={AWS_REGION_US_EAST_2},
|
||||
)
|
||||
|
||||
assert aws_provider._excluded_regions == {AWS_REGION_US_EAST_2}
|
||||
assert AWS_REGION_US_EAST_2 not in aws_provider._enabled_regions
|
||||
assert AWS_REGION_EU_WEST_1 in aws_provider._enabled_regions
|
||||
assert AWS_REGION_US_EAST_1 in aws_provider._enabled_regions
|
||||
finally:
|
||||
os.remove(config_path)
|
||||
|
||||
@mock_aws
|
||||
def test_excluded_regions_from_config_avoid_excluded_profile_region(
|
||||
self, monkeypatch
|
||||
):
|
||||
monkeypatch.setenv("AWS_DEFAULT_REGION", AWS_REGION_EU_WEST_1)
|
||||
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as tmp:
|
||||
tmp.write(f"aws:\n disallowed_regions:\n - {AWS_REGION_EU_WEST_1}\n")
|
||||
config_path = tmp.name
|
||||
try:
|
||||
aws_provider = AwsProvider(config_path=config_path)
|
||||
|
||||
assert aws_provider.identity.profile_region == AWS_REGION_US_EAST_1
|
||||
finally:
|
||||
os.remove(config_path)
|
||||
|
||||
@mock_aws
|
||||
def test_aws_provider_raises_when_all_input_regions_are_excluded(self):
|
||||
with raises(AWSArgumentTypeValidationError):
|
||||
AwsProvider(
|
||||
regions={AWS_REGION_EU_WEST_1},
|
||||
excluded_regions={AWS_REGION_EU_WEST_1},
|
||||
)
|
||||
|
||||
def test_get_excluded_regions_from_env_parses_comma_list(self):
|
||||
with mock.patch.dict(
|
||||
os.environ,
|
||||
{"PROWLER_AWS_DISALLOWED_REGIONS": " me-south-1 , ap-east-1 ,, "},
|
||||
):
|
||||
assert Provider.get_excluded_regions_from_env() == {
|
||||
"me-south-1",
|
||||
"ap-east-1",
|
||||
}
|
||||
|
||||
def test_get_excluded_regions_from_env_ignores_legacy_generic_name(self):
|
||||
with mock.patch.dict(
|
||||
os.environ,
|
||||
{"PROWLER_DISALLOWED_REGIONS": "me-south-1"},
|
||||
clear=True,
|
||||
):
|
||||
assert Provider.get_excluded_regions_from_env() == set()
|
||||
|
||||
def test_get_excluded_regions_from_env_unset(self):
|
||||
with mock.patch.dict(os.environ, {}, clear=True):
|
||||
assert Provider.get_excluded_regions_from_env() == set()
|
||||
|
||||
@mock_aws
|
||||
def test_print_credentials_shows_all_except_excluded_regions(self):
|
||||
aws_provider = AwsProvider(
|
||||
excluded_regions={AWS_REGION_EU_WEST_1, AWS_REGION_US_EAST_1}
|
||||
)
|
||||
|
||||
with patch(
|
||||
"prowler.providers.aws.aws_provider.print_boxes"
|
||||
) as mock_print_boxes:
|
||||
aws_provider.print_credentials()
|
||||
|
||||
report_lines = mock_print_boxes.call_args.args[0]
|
||||
assert any(
|
||||
"AWS Regions:" in line and "all except eu-west-1, us-east-1" in line
|
||||
for line in report_lines
|
||||
)
|
||||
|
||||
@mock_aws
|
||||
def test_generate_regional_clients_all_enabled_regions(self):
|
||||
aws_provider = AwsProvider()
|
||||
@@ -2033,6 +2159,24 @@ aws:
|
||||
== AWS_REGION_EU_WEST_1
|
||||
)
|
||||
|
||||
def test_get_aws_region_for_sts_avoids_excluded_session_region(self):
|
||||
input_regions = None
|
||||
session_region = AWS_REGION_EU_WEST_1
|
||||
assert (
|
||||
get_aws_region_for_sts(
|
||||
session_region, input_regions, {AWS_REGION_EU_WEST_1}
|
||||
)
|
||||
== AWS_REGION_US_EAST_1
|
||||
)
|
||||
|
||||
def test_get_profile_region_avoids_excluded_session_region(self):
|
||||
mocked_session = mock.Mock(region_name=AWS_REGION_EU_WEST_1)
|
||||
|
||||
assert (
|
||||
AwsProvider.get_profile_region(mocked_session, {AWS_REGION_EU_WEST_1})
|
||||
== AWS_REGION_US_EAST_1
|
||||
)
|
||||
|
||||
@mock_aws
|
||||
def test_set_session_config_default(self):
|
||||
aws_provider = AwsProvider()
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
All notable changes to the **Prowler UI** are documented in this file.
|
||||
|
||||
## [1.24.0] (Prowler UNRELEASED)
|
||||
## [1.24.0] (Prowler v5.24.0)
|
||||
|
||||
### 🚀 Added
|
||||
|
||||
@@ -12,11 +12,9 @@ All notable changes to the **Prowler UI** are documented in this file.
|
||||
### 🔄 Changed
|
||||
|
||||
- Attack Paths scan selection: contextual button labels based on graph availability, tooltips on disabled actions, green dot indicator for selectable scans, and a warning banner when viewing data from a previous scan cycle [(#10685)](https://github.com/prowler-cloud/prowler/pull/10685)
|
||||
|
||||
### 🔄 Changed
|
||||
|
||||
- Remove legacy finding detail sheet, row-details wrapper, and resource detail panel; unify findings and resources around new side drawers [(#10692)](https://github.com/prowler-cloud/prowler/pull/10692)
|
||||
- Attack Paths "View Finding" now opens the finding drawer inline over the graph instead of navigating to `/findings` in a new tab, preserving graph zoom, selection, and filter state
|
||||
- Attack Paths scan table: replace action buttons with radio buttons, add dedicated Graph column, use info-colored In Progress badge, remove redundant Progress column, and fix info banner variant [(#10704)](https://github.com/prowler-cloud/prowler/pull/10704)
|
||||
|
||||
### 🐞 Fixed
|
||||
|
||||
@@ -51,8 +49,9 @@ All notable changes to the **Prowler UI** are documented in this file.
|
||||
### 🐞 Fixed
|
||||
|
||||
- Preserve query parameters in callbackUrl during invitation flow [(#10571)](https://github.com/prowler-cloud/prowler/pull/10571)
|
||||
- Attack Paths scan auto-refresh now correctly detects "available" (queued) scans as active [(#10476)](https://github.com/prowler-cloud/prowler/pull/10476)
|
||||
- Attack Paths empty state not showing when no scans exist [(#10469)](https://github.com/prowler-cloud/prowler/pull/10469)
|
||||
- Clear Filters now resets all filters including muted findings and auto-applies, Clear all in pills only removes pill-visible sub-filters, and the discard icon is now an Undo text button [(#10446)](https://github.com/prowler-cloud/prowler/pull/10446)
|
||||
- Send to Jira modal now dynamically fetches and displays available issue types per project instead of hardcoding `"Task"`, fixing failures on non-English Jira instances [(#10534)](https://github.com/prowler-cloud/prowler/pull/10534)
|
||||
- Exclude service filter from finding group resources endpoint to prevent empty results when a service filter is active [(#10652)](https://github.com/prowler-cloud/prowler/pull/10652)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -169,14 +169,14 @@ describe("ScanListTable", () => {
|
||||
expect(screen.getByText("12 Total Entries")).toBeInTheDocument();
|
||||
expect(screen.getByText("Page 1 of 3")).toBeInTheDocument();
|
||||
|
||||
await user.click(screen.getAllByRole("button", { name: "Select scan" })[0]);
|
||||
await user.click(screen.getAllByRole("radio", { name: "Select scan" })[0]);
|
||||
|
||||
expect(pushMock).toHaveBeenCalledWith(
|
||||
"/attack-paths?scanPage=1&scanPageSize=5&scanId=scan-1",
|
||||
);
|
||||
});
|
||||
|
||||
it("enables the select button for a failed scan when graph data is ready", async () => {
|
||||
it("enables the radio button for a failed scan when graph data is ready", async () => {
|
||||
const user = userEvent.setup();
|
||||
const failedScan: AttackPathScan = {
|
||||
...createScan(1),
|
||||
@@ -189,18 +189,18 @@ describe("ScanListTable", () => {
|
||||
|
||||
render(<ScanListTable scans={[failedScan]} />);
|
||||
|
||||
const button = screen.getByRole("button", { name: "Select scan" });
|
||||
expect(button).toBeEnabled();
|
||||
expect(button).toHaveTextContent("Select");
|
||||
const radio = screen.getByRole("radio", { name: "Select scan" });
|
||||
expect(radio).toBeEnabled();
|
||||
expect(radio).toHaveAttribute("aria-checked", "false");
|
||||
|
||||
await user.click(button);
|
||||
await user.click(radio);
|
||||
|
||||
expect(pushMock).toHaveBeenCalledWith(
|
||||
"/attack-paths?scanPage=1&scanPageSize=5&scanId=scan-1",
|
||||
);
|
||||
});
|
||||
|
||||
it("disables the select button for a failed scan when graph data is not ready", () => {
|
||||
it("disables the radio button for a failed scan when graph data is not ready", () => {
|
||||
const failedScan: AttackPathScan = {
|
||||
...createScan(1),
|
||||
attributes: {
|
||||
@@ -212,12 +212,11 @@ describe("ScanListTable", () => {
|
||||
|
||||
render(<ScanListTable scans={[failedScan]} />);
|
||||
|
||||
const button = screen.getByRole("button", { name: "Select scan" });
|
||||
expect(button).toBeDisabled();
|
||||
expect(button).toHaveTextContent("Failed");
|
||||
const radio = screen.getByRole("radio", { name: "Scan not available" });
|
||||
expect(radio).toBeDisabled();
|
||||
});
|
||||
|
||||
it("shows 'Scheduled' label for a scheduled scan without graph data", () => {
|
||||
it("shows a disabled radio button for a scheduled scan without graph data", () => {
|
||||
const scheduledScan: AttackPathScan = {
|
||||
...createScan(1),
|
||||
attributes: {
|
||||
@@ -232,12 +231,11 @@ describe("ScanListTable", () => {
|
||||
|
||||
render(<ScanListTable scans={[scheduledScan]} />);
|
||||
|
||||
const button = screen.getByRole("button", { name: "Select scan" });
|
||||
expect(button).toBeDisabled();
|
||||
expect(button).toHaveTextContent("Scheduled");
|
||||
const radio = screen.getByRole("radio", { name: "Scan not available" });
|
||||
expect(radio).toBeDisabled();
|
||||
});
|
||||
|
||||
it("shows 'Running...' label for an executing scan without graph data", () => {
|
||||
it("shows a disabled radio button for an executing scan without graph data", () => {
|
||||
const executingScan: AttackPathScan = {
|
||||
...createScan(1),
|
||||
attributes: {
|
||||
@@ -252,12 +250,11 @@ describe("ScanListTable", () => {
|
||||
|
||||
render(<ScanListTable scans={[executingScan]} />);
|
||||
|
||||
const button = screen.getByRole("button", { name: "Select scan" });
|
||||
expect(button).toBeDisabled();
|
||||
expect(button).toHaveTextContent("Running...");
|
||||
const radio = screen.getByRole("radio", { name: "Scan not available" });
|
||||
expect(radio).toBeDisabled();
|
||||
});
|
||||
|
||||
it("enables Select for a scheduled scan when graph data is ready from a previous cycle", async () => {
|
||||
it("enables the radio button for a scheduled scan when graph data is ready from a previous cycle", async () => {
|
||||
const user = userEvent.setup();
|
||||
const scheduledWithGraph: AttackPathScan = {
|
||||
...createScan(1),
|
||||
@@ -271,26 +268,26 @@ describe("ScanListTable", () => {
|
||||
|
||||
render(<ScanListTable scans={[scheduledWithGraph]} />);
|
||||
|
||||
const button = screen.getByRole("button", { name: "Select scan" });
|
||||
expect(button).toBeEnabled();
|
||||
expect(button).toHaveTextContent("Select");
|
||||
const radio = screen.getByRole("radio", { name: "Select scan" });
|
||||
expect(radio).toBeEnabled();
|
||||
expect(radio).toHaveAttribute("aria-checked", "false");
|
||||
|
||||
await user.click(button);
|
||||
await user.click(radio);
|
||||
|
||||
expect(pushMock).toHaveBeenCalledWith(
|
||||
"/attack-paths?scanPage=1&scanPageSize=5&scanId=scan-1",
|
||||
);
|
||||
});
|
||||
|
||||
it("shows a green dot next to the account name when graph data is ready", () => {
|
||||
it("exposes an accessible label in the Graph column when graph data is ready", () => {
|
||||
render(<ScanListTable scans={[createScan(1)]} />);
|
||||
|
||||
const dot = screen.getByLabelText("Graph data available");
|
||||
expect(dot).toBeInTheDocument();
|
||||
expect(dot).toHaveClass("bg-bg-pass-primary");
|
||||
expect(screen.getByLabelText("Graph available")).toHaveClass(
|
||||
"text-text-success-primary",
|
||||
);
|
||||
});
|
||||
|
||||
it("does not show a green dot when graph data is not ready", () => {
|
||||
it("exposes an accessible label in the Graph column when graph data is not ready", () => {
|
||||
const noGraphScan: AttackPathScan = {
|
||||
...createScan(1),
|
||||
attributes: {
|
||||
@@ -301,8 +298,28 @@ describe("ScanListTable", () => {
|
||||
|
||||
render(<ScanListTable scans={[noGraphScan]} />);
|
||||
|
||||
expect(screen.getByLabelText("Graph not available")).toHaveClass(
|
||||
"text-text-neutral-secondary",
|
||||
);
|
||||
});
|
||||
|
||||
it("renders a tooltip explaining a completed scan without graph data", () => {
|
||||
const completedNoGraph: AttackPathScan = {
|
||||
...createScan(1),
|
||||
attributes: {
|
||||
...createScan(1).attributes,
|
||||
state: "completed",
|
||||
graph_data_ready: false,
|
||||
},
|
||||
};
|
||||
|
||||
render(<ScanListTable scans={[completedNoGraph]} />);
|
||||
|
||||
expect(
|
||||
screen.queryByLabelText("Graph data available"),
|
||||
).not.toBeInTheDocument();
|
||||
screen.getByRole("radio", { name: "Scan not available" }),
|
||||
).toBeDisabled();
|
||||
expect(
|
||||
screen.getByText("This scan completed without producing graph data."),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { Check, Minus } from "lucide-react";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
|
||||
import { Button } from "@/components/shadcn/button/button";
|
||||
import {
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
} from "@/components/shadcn/radio-group/radio-group";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
@@ -38,44 +42,6 @@ const formatNullableDuration = (duration: number | null) => {
|
||||
return formatDuration(duration);
|
||||
};
|
||||
|
||||
const isSelectDisabled = (
|
||||
scan: AttackPathScan,
|
||||
selectedScanId: string | null,
|
||||
) => {
|
||||
return !scan.attributes.graph_data_ready || selectedScanId === scan.id;
|
||||
};
|
||||
|
||||
const getSelectButtonLabel = (
|
||||
scan: AttackPathScan,
|
||||
selectedScanId: string | null,
|
||||
) => {
|
||||
if (selectedScanId === scan.id) {
|
||||
return "Selected";
|
||||
}
|
||||
|
||||
if (scan.attributes.graph_data_ready) {
|
||||
return "Select";
|
||||
}
|
||||
|
||||
if (scan.attributes.state === SCAN_STATES.SCHEDULED) {
|
||||
return "Scheduled";
|
||||
}
|
||||
|
||||
if (scan.attributes.state === SCAN_STATES.AVAILABLE) {
|
||||
return "Queued";
|
||||
}
|
||||
|
||||
if (scan.attributes.state === SCAN_STATES.EXECUTING) {
|
||||
return "Running...";
|
||||
}
|
||||
|
||||
if (scan.attributes.state === SCAN_STATES.FAILED) {
|
||||
return "Failed";
|
||||
}
|
||||
|
||||
return "Select";
|
||||
};
|
||||
|
||||
const getDisabledTooltip = (scan: AttackPathScan): string | null => {
|
||||
if (scan.attributes.graph_data_ready) {
|
||||
return null;
|
||||
@@ -97,7 +63,11 @@ const getDisabledTooltip = (scan: AttackPathScan): string | null => {
|
||||
return "This scan failed. No graph data is available.";
|
||||
}
|
||||
|
||||
return null;
|
||||
if (scan.attributes.state === SCAN_STATES.COMPLETED) {
|
||||
return "This scan completed without producing graph data.";
|
||||
}
|
||||
|
||||
return "Graph data is not available for this scan.";
|
||||
};
|
||||
|
||||
const getSelectedRowSelection = (
|
||||
@@ -129,37 +99,65 @@ const buildMetadata = (
|
||||
|
||||
const getColumns = ({
|
||||
selectedScanId,
|
||||
onSelectScan,
|
||||
}: {
|
||||
selectedScanId: string | null;
|
||||
onSelectScan: (scanId: string) => void;
|
||||
}): ColumnDef<AttackPathScan>[] => [
|
||||
{
|
||||
id: "select",
|
||||
header: () => <span className="text-sm font-medium">Select</span>,
|
||||
cell: ({ row }) => {
|
||||
const isSelected = selectedScanId === row.original.id;
|
||||
const canSelect = row.original.attributes.graph_data_ready;
|
||||
const tooltip = getDisabledTooltip(row.original);
|
||||
|
||||
const radio = (
|
||||
<RadioGroupItem
|
||||
value={row.original.id}
|
||||
checked={isSelected}
|
||||
disabled={!canSelect}
|
||||
className={cn(
|
||||
"size-5",
|
||||
canSelect &&
|
||||
!isSelected &&
|
||||
"border-text-neutral-secondary cursor-pointer",
|
||||
!canSelect && "disabled:opacity-70",
|
||||
)}
|
||||
aria-label={
|
||||
isSelected
|
||||
? "Selected scan"
|
||||
: canSelect
|
||||
? "Select scan"
|
||||
: "Scan not available"
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
if (!canSelect && !isSelected && tooltip) {
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span tabIndex={0}>{radio}</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{tooltip}</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return radio;
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "provider",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Account" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className={cn(
|
||||
"inline-block size-2 shrink-0 rounded-full",
|
||||
row.original.attributes.graph_data_ready
|
||||
? "bg-bg-pass-primary"
|
||||
: "bg-transparent",
|
||||
)}
|
||||
aria-label={
|
||||
row.original.attributes.graph_data_ready
|
||||
? "Graph data available"
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<EntityInfo
|
||||
cloudProvider={row.original.attributes.provider_type as ProviderType}
|
||||
entityAlias={row.original.attributes.provider_alias}
|
||||
entityId={row.original.attributes.provider_uid}
|
||||
/>
|
||||
</div>
|
||||
<EntityInfo
|
||||
cloudProvider={row.original.attributes.provider_type as ProviderType}
|
||||
entityAlias={row.original.attributes.provider_alias}
|
||||
entityId={row.original.attributes.provider_uid}
|
||||
/>
|
||||
),
|
||||
enableSorting: false,
|
||||
},
|
||||
@@ -182,22 +180,32 @@ const getColumns = ({
|
||||
<DataTableColumnHeader column={column} title="Status" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<ScanStatusBadge
|
||||
status={row.original.attributes.state}
|
||||
progress={row.original.attributes.progress}
|
||||
graphDataReady={row.original.attributes.graph_data_ready}
|
||||
/>
|
||||
<div className="flex">
|
||||
<ScanStatusBadge
|
||||
status={row.original.attributes.state}
|
||||
progress={row.original.attributes.progress}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "progress",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Progress" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<span className="text-sm">{row.original.attributes.progress}%</span>
|
||||
),
|
||||
accessorKey: "graph_data_ready",
|
||||
header: () => <span className="text-sm font-medium">Graph</span>,
|
||||
cell: ({ row }) =>
|
||||
row.original.attributes.graph_data_ready ? (
|
||||
<Check
|
||||
size={16}
|
||||
aria-label="Graph available"
|
||||
className="text-text-success-primary"
|
||||
/>
|
||||
) : (
|
||||
<Minus
|
||||
size={16}
|
||||
aria-label="Graph not available"
|
||||
className="text-text-neutral-secondary"
|
||||
/>
|
||||
),
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
@@ -212,45 +220,6 @@ const getColumns = ({
|
||||
),
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: () => <span className="sr-only">Actions</span>,
|
||||
cell: ({ row }) => {
|
||||
const isDisabled = isSelectDisabled(row.original, selectedScanId);
|
||||
const tooltip = getDisabledTooltip(row.original);
|
||||
|
||||
const button = (
|
||||
<Button
|
||||
type="button"
|
||||
aria-label="Select scan"
|
||||
disabled={isDisabled}
|
||||
variant={isDisabled ? "secondary" : "default"}
|
||||
onClick={() => onSelectScan(row.original.id)}
|
||||
className="w-full max-w-24"
|
||||
>
|
||||
{getSelectButtonLabel(row.original, selectedScanId)}
|
||||
</Button>
|
||||
);
|
||||
|
||||
if (isDisabled && tooltip) {
|
||||
return (
|
||||
<div className="flex justify-end">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="w-full max-w-24" tabIndex={0}>
|
||||
{button}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{tooltip}</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <div className="flex justify-end">{button}</div>;
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -300,19 +269,27 @@ export const ScanListTable = ({ scans }: ScanListTableProps) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
columns={getColumns({
|
||||
selectedScanId,
|
||||
onSelectScan: handleSelectScan,
|
||||
})}
|
||||
data={paginatedScans}
|
||||
metadata={buildMetadata(scans.length, currentPage, totalPages)}
|
||||
controlledPage={currentPage}
|
||||
controlledPageSize={pageSize}
|
||||
onPageChange={handlePageChange}
|
||||
onPageSizeChange={handlePageSizeChange}
|
||||
enableRowSelection
|
||||
rowSelection={getSelectedRowSelection(paginatedScans, selectedScanId)}
|
||||
/>
|
||||
<RadioGroup
|
||||
value={selectedScanId ?? ""}
|
||||
onValueChange={handleSelectScan}
|
||||
className="gap-0"
|
||||
>
|
||||
<DataTable
|
||||
columns={getColumns({ selectedScanId })}
|
||||
data={paginatedScans}
|
||||
metadata={buildMetadata(scans.length, currentPage, totalPages)}
|
||||
controlledPage={currentPage}
|
||||
controlledPageSize={pageSize}
|
||||
onPageChange={handlePageChange}
|
||||
onPageSizeChange={handlePageSizeChange}
|
||||
onRowClick={(row) => {
|
||||
if (row.original.attributes.graph_data_ready) {
|
||||
handleSelectScan(row.original.id);
|
||||
}
|
||||
}}
|
||||
enableRowSelection
|
||||
rowSelection={getSelectedRowSelection(paginatedScans, selectedScanId)}
|
||||
/>
|
||||
</RadioGroup>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,97 +3,55 @@
|
||||
import { Loader2 } from "lucide-react";
|
||||
|
||||
import { Badge } from "@/components/shadcn/badge/badge";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/shadcn/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { ScanState } from "@/types/attack-paths";
|
||||
import { SCAN_STATES } from "@/types/attack-paths";
|
||||
|
||||
const BADGE_CONFIG: Record<
|
||||
ScanState,
|
||||
{ className: string; label: string; showGraphDot: boolean }
|
||||
> = {
|
||||
const BADGE_CONFIG: Record<ScanState, { className: string; label: string }> = {
|
||||
[SCAN_STATES.SCHEDULED]: {
|
||||
className: "bg-bg-neutral-tertiary text-text-neutral-primary",
|
||||
label: "Scheduled",
|
||||
showGraphDot: true,
|
||||
},
|
||||
[SCAN_STATES.AVAILABLE]: {
|
||||
className: "bg-bg-neutral-tertiary text-text-neutral-primary",
|
||||
label: "Queued",
|
||||
showGraphDot: true,
|
||||
},
|
||||
[SCAN_STATES.EXECUTING]: {
|
||||
className: "bg-bg-warning-secondary text-text-neutral-primary",
|
||||
className: "bg-bg-info-secondary text-text-info",
|
||||
label: "In Progress",
|
||||
showGraphDot: false,
|
||||
},
|
||||
[SCAN_STATES.COMPLETED]: {
|
||||
className: "bg-bg-pass-secondary text-text-success-primary",
|
||||
label: "Completed",
|
||||
showGraphDot: false,
|
||||
},
|
||||
[SCAN_STATES.FAILED]: {
|
||||
className: "bg-bg-fail-secondary text-text-error-primary",
|
||||
label: "Failed",
|
||||
showGraphDot: true,
|
||||
},
|
||||
};
|
||||
|
||||
interface ScanStatusBadgeProps {
|
||||
status: ScanState;
|
||||
progress?: number;
|
||||
graphDataReady?: boolean;
|
||||
}
|
||||
|
||||
export const ScanStatusBadge = ({
|
||||
status,
|
||||
progress = 0,
|
||||
graphDataReady = false,
|
||||
}: ScanStatusBadgeProps) => {
|
||||
const config = BADGE_CONFIG[status];
|
||||
|
||||
const graphDot = graphDataReady && config.showGraphDot && (
|
||||
<span className="bg-bg-pass-primary inline-block size-2 rounded-full" />
|
||||
);
|
||||
|
||||
const tooltipText = graphDataReady
|
||||
? "Graph available"
|
||||
: status === SCAN_STATES.FAILED || status === SCAN_STATES.COMPLETED
|
||||
? "Graph not available"
|
||||
: "Graph not available yet";
|
||||
|
||||
const icon =
|
||||
status === SCAN_STATES.EXECUTING ? (
|
||||
<Loader2
|
||||
size={14}
|
||||
className={
|
||||
graphDataReady
|
||||
? "text-text-success-primary animate-spin"
|
||||
: "animate-spin"
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
graphDot
|
||||
);
|
||||
|
||||
const label =
|
||||
status === SCAN_STATES.EXECUTING
|
||||
? `${config.label} (${progress}%)`
|
||||
: config.label;
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Badge className={cn(config.className, "gap-2")}>
|
||||
{icon}
|
||||
<span>{label}</span>
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{tooltipText}</TooltipContent>
|
||||
</Tooltip>
|
||||
<Badge className={cn(config.className, "gap-2")}>
|
||||
{status === SCAN_STATES.EXECUTING && (
|
||||
<Loader2 size={14} className="animate-spin" />
|
||||
)}
|
||||
<span>{label}</span>
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { ArrowLeft, Info, Maximize2, TriangleAlert, X } from "lucide-react";
|
||||
import { ArrowLeft, Info, Maximize2, X } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { Suspense, useEffect, useRef, useState } from "react";
|
||||
@@ -120,8 +120,8 @@ export default function AttackPathsPage() {
|
||||
// Check if there's an executing scan for auto-refresh
|
||||
const hasExecutingScan = scans.some(
|
||||
(scan) =>
|
||||
scan.attributes.state === "executing" ||
|
||||
scan.attributes.state === "scheduled",
|
||||
scan.attributes.state === SCAN_STATES.EXECUTING ||
|
||||
scan.attributes.state === SCAN_STATES.SCHEDULED,
|
||||
);
|
||||
|
||||
// Detect if the selected scan is showing data from a previous cycle
|
||||
@@ -358,11 +358,11 @@ export default function AttackPathsPage() {
|
||||
<h2 className="dark:text-prowler-theme-pale/90 text-xl font-semibold">
|
||||
Attack Paths
|
||||
</h2>
|
||||
<p className="text-text-neutral-secondary dark:text-text-neutral-secondary mt-2 text-sm">
|
||||
<p className="text-text-neutral-secondary mt-2 text-sm">
|
||||
Select a scan, build a query, and visualize Attack Paths in your
|
||||
infrastructure.
|
||||
</p>
|
||||
<p className="text-text-neutral-secondary dark:text-text-neutral-secondary mt-1 text-xs">
|
||||
<p className="text-text-neutral-secondary mt-1 text-xs">
|
||||
Scans can be selected when data is available. A new scan does not
|
||||
interrupt access to existing data.
|
||||
</p>
|
||||
@@ -394,11 +394,8 @@ export default function AttackPathsPage() {
|
||||
|
||||
{/* Banner: viewing data from a previous scan cycle */}
|
||||
{isViewingPreviousCycleData && (
|
||||
<Alert
|
||||
variant="default"
|
||||
className="border-border-warning-secondary bg-bg-warning-secondary"
|
||||
>
|
||||
<TriangleAlert className="text-text-warning-primary size-4" />
|
||||
<Alert variant="info">
|
||||
<Info className="size-4" />
|
||||
<AlertTitle>Viewing data from a previous scan</AlertTitle>
|
||||
<AlertDescription>
|
||||
This scan is currently{" "}
|
||||
@@ -605,7 +602,7 @@ export default function AttackPathsPage() {
|
||||
<X size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-text-neutral-secondary dark:text-text-neutral-secondary mb-4 text-xs">
|
||||
<p className="text-text-neutral-secondary mb-4 text-xs">
|
||||
{graphState.selectedNode?.labels.some(
|
||||
(label) =>
|
||||
label
|
||||
@@ -628,7 +625,7 @@ export default function AttackPathsPage() {
|
||||
<h4 className="mb-2 text-xs font-semibold">
|
||||
Type
|
||||
</h4>
|
||||
<p className="text-text-neutral-secondary dark:text-text-neutral-secondary text-xs">
|
||||
<p className="text-text-neutral-secondary text-xs">
|
||||
{graphState.selectedNode?.labels
|
||||
.map(formatNodeLabel)
|
||||
.join(", ")}
|
||||
@@ -678,7 +675,7 @@ export default function AttackPathsPage() {
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-semibold">Node Details</h3>
|
||||
<p className="text-text-neutral-secondary dark:text-text-neutral-secondary mt-1 text-sm">
|
||||
<p className="text-text-neutral-secondary mt-1 text-sm">
|
||||
{String(
|
||||
graphState.selectedNode.labels.some((label) =>
|
||||
label.toLowerCase().includes("finding"),
|
||||
|
||||
Reference in New Issue
Block a user