From dd6d8c71fdd0e7a3c2801fa1a3c1ec9755b49ca6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Feb 2025 12:58:48 -0500 Subject: [PATCH 01/30] chore(deps-dev): bump moto from 5.0.27 to 5.0.28 (#6804) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: MrCloudSec --- poetry.lock | 21 +++++++-------------- pyproject.toml | 4 ++-- tests/lib/outputs/ocsf/ocsf_test.py | 2 +- 3 files changed, 10 insertions(+), 17 deletions(-) diff --git a/poetry.lock b/poetry.lock index 1a3d754e90..93e5fc2cfe 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2050,8 +2050,6 @@ optional = false python-versions = "*" files = [ {file = "jsonpath-ng-1.7.0.tar.gz", hash = "sha256:f6f5f7fd4e5ff79c785f1573b394043b39849fb2bb47bcead935d12b00beab3c"}, - {file = "jsonpath_ng-1.7.0-py2-none-any.whl", hash = "sha256:898c93fc173f0c336784a3fa63d7434297544b7198124a68f9a3ef9597b0ae6e"}, - {file = "jsonpath_ng-1.7.0-py3-none-any.whl", hash = "sha256:f3d7f9e848cba1b6da28c55b1c26ff915dc9e0b1ba7e752a53d6da8d5cbd00b6"}, ] [package.dependencies] @@ -2592,13 +2590,13 @@ test = ["pytest", "pytest-cov"] [[package]] name = "moto" -version = "5.0.27" +version = "5.0.28" description = "A library that allows you to easily mock out tests based on AWS infrastructure" optional = false python-versions = ">=3.8" files = [ - {file = "moto-5.0.27-py3-none-any.whl", hash = "sha256:27042fd94c8def0166d9f2ae8d39d9488d4b3115542b5fca88566c0424549013"}, - {file = "moto-5.0.27.tar.gz", hash = "sha256:6c123de7e0e5e6508a10c399ba3ecf2d5143f263f8e804fd4a7091941c3f5207"}, + {file = "moto-5.0.28-py3-none-any.whl", hash = "sha256:2dfbea1afe3b593e13192059a1a7fc4b3cf7fdf92e432070c22346efa45aa0f0"}, + {file = "moto-5.0.28.tar.gz", hash = "sha256:4d3437693411ec943c13c77de5b0b520c4b0a9ac850fead4ba2a54709e086e8b"}, ] [package.dependencies] @@ -3454,13 +3452,13 @@ test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] [[package]] name = "py-ocsf-models" -version = "0.2.0" +version = "0.3.0" description = "This is a Python implementation of the OCSF models. The models are used to represent the data of the OCSF Schema defined in https://schema.ocsf.io/." optional = false python-versions = "<3.13,>=3.9" files = [ - {file = "py_ocsf_models-0.2.0-py3-none-any.whl", hash = "sha256:ac75fd21077694b343ebaad3479194db113c274879b114277560ff287d5cd7b5"}, - {file = "py_ocsf_models-0.2.0.tar.gz", hash = "sha256:3e12648d05329e6776a0e6b1ffea87a3eb60aa7d8cb2c4afd69e5724f443ce03"}, + {file = "py_ocsf_models-0.3.0-py3-none-any.whl", hash = "sha256:3d31e379be5e4271f7faf62dee9c36798559a1f7f98dff142c0e4cfdb35e291c"}, + {file = "py_ocsf_models-0.3.0.tar.gz", hash = "sha256:ad46b7d9761b74010f06a894df2d9541989252b7ff738cd5c7edbf4283df2279"}, ] [package.dependencies] @@ -4310,7 +4308,6 @@ files = [ {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f66efbc1caa63c088dead1c4170d148eabc9b80d95fb75b6c92ac0aad2437d76"}, {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:22353049ba4181685023b25b5b51a574bce33e7f51c759371a7422dcae5402a6"}, {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:932205970b9f9991b34f55136be327501903f7c66830e9760a8ffb15b07f05cd"}, - {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a52d48f4e7bf9005e8f0a89209bf9a73f7190ddf0489eee5eb51377385f59f2a"}, {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win32.whl", hash = "sha256:3eac5a91891ceb88138c113f9db04f3cebdae277f5d44eaa3651a4f573e6a5da"}, {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win_amd64.whl", hash = "sha256:ab007f2f5a87bd08ab1499bdf96f3d5c6ad4dcfa364884cb4549aa0154b13a28"}, {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:4a6679521a58256a90b0d89e03992c15144c5f3858f40d7c18886023d7943db6"}, @@ -4319,7 +4316,6 @@ files = [ {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:811ea1594b8a0fb466172c384267a4e5e367298af6b228931f273b111f17ef52"}, {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cf12567a7b565cbf65d438dec6cfbe2917d3c1bdddfce84a9930b7d35ea59642"}, {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7dd5adc8b930b12c8fc5b99e2d535a09889941aa0d0bd06f4749e9a9397c71d2"}, - {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1492a6051dab8d912fc2adeef0e8c72216b24d57bd896ea607cb90bb0c4981d3"}, {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win32.whl", hash = "sha256:bd0a08f0bab19093c54e18a14a10b4322e1eacc5217056f3c063bd2f59853ce4"}, {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win_amd64.whl", hash = "sha256:a274fb2cb086c7a3dea4322ec27f4cb5cc4b6298adb583ab0e211a4682f241eb"}, {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:20b0f8dc160ba83b6dcc0e256846e1a02d044e13f7ea74a3d1d56ede4e48c632"}, @@ -4328,7 +4324,6 @@ files = [ {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:749c16fcc4a2b09f28843cda5a193e0283e47454b63ec4b81eaa2242f50e4ccd"}, {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bf165fef1f223beae7333275156ab2022cffe255dcc51c27f066b4370da81e31"}, {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:32621c177bbf782ca5a18ba4d7af0f1082a3f6e517ac2a18b3974d4edf349680"}, - {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b82a7c94a498853aa0b272fd5bc67f29008da798d4f93a2f9f289feb8426a58d"}, {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win32.whl", hash = "sha256:e8c4ebfcfd57177b572e2040777b8abc537cdef58a2120e830124946aa9b42c5"}, {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win_amd64.whl", hash = "sha256:0467c5965282c62203273b838ae77c0d29d7638c8a4e3a1c8bdd3602c10904e4"}, {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4c8c5d82f50bb53986a5e02d1b3092b03622c02c2eb78e29bec33fd9593bae1a"}, @@ -4337,7 +4332,6 @@ files = [ {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96777d473c05ee3e5e3c3e999f5d23c6f4ec5b0c38c098b3a5229085f74236c6"}, {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:3bc2a80e6420ca8b7d3590791e2dfc709c88ab9152c00eeb511c9875ce5778bf"}, {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e188d2699864c11c36cdfdada94d781fd5d6b0071cd9c427bceb08ad3d7c70e1"}, - {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f6f3eac23941b32afccc23081e1f50612bdbe4e982012ef4f5797986828cd01"}, {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win32.whl", hash = "sha256:6442cb36270b3afb1b4951f060eccca1ce49f3d087ca1ca4563a6eb479cb3de6"}, {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win_amd64.whl", hash = "sha256:e5b8daf27af0b90da7bb903a876477a9e6d7270be6146906b276605997c7e9a3"}, {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:fc4b630cd3fa2cf7fce38afa91d7cfe844a9f75d7f0f36393fa98815e911d987"}, @@ -4346,7 +4340,6 @@ files = [ {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2f1c3765db32be59d18ab3953f43ab62a761327aafc1594a2a1fbe038b8b8a7"}, {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d85252669dc32f98ebcd5d36768f5d4faeaeaa2d655ac0473be490ecdae3c285"}, {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e143ada795c341b56de9418c58d028989093ee611aa27ffb9b7f609c00d813ed"}, - {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2c59aa6170b990d8d2719323e628aaf36f3bfbc1c26279c0eeeb24d05d2d11c7"}, {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win32.whl", hash = "sha256:beffaed67936fbbeffd10966a4eb53c402fafd3d6833770516bf7314bc6ffa12"}, {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win_amd64.whl", hash = "sha256:040ae85536960525ea62868b642bdb0c2cc6021c9f9d507810c0c604e66f5a7b"}, {file = "ruamel.yaml.clib-0.2.12.tar.gz", hash = "sha256:6c8fbb13ec503f99a91901ab46e0b07ae7941cd527393187039aec586fdfd36f"}, @@ -5090,4 +5083,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.13" -content-hash = "c387413f38df1dc2286d8e16077fdccd2573943bf7cbdaeca2389be0ee0c339c" +content-hash = "d4d817d0a3690b7a39222bf79420e3adb764b47c6c1d2ee0c73a7599333114b1" diff --git a/pyproject.toml b/pyproject.toml index b6d6091419..1fb1ab82a2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,7 +63,7 @@ microsoft-kiota-abstractions = "1.9.1" msgraph-sdk = "1.18.0" numpy = "2.0.2" pandas = "2.2.3" -py-ocsf-models = "0.2.0" +py-ocsf-models = "0.3.0" pydantic = "1.10.18" python = ">=3.9,<3.13" python-dateutil = "^2.9.0.post0" @@ -82,7 +82,7 @@ docker = "7.1.0" flake8 = "7.1.1" freezegun = "1.5.1" mock = "5.1.0" -moto = {extras = ["all"], version = "5.0.27"} +moto = {extras = ["all"], version = "5.0.28"} openapi-schema-validator = "0.6.3" openapi-spec-validator = "0.7.1" pylint = "3.3.4" diff --git a/tests/lib/outputs/ocsf/ocsf_test.py b/tests/lib/outputs/ocsf/ocsf_test.py index e7bd8b2059..d272849905 100644 --- a/tests/lib/outputs/ocsf/ocsf_test.py +++ b/tests/lib/outputs/ocsf/ocsf_test.py @@ -175,7 +175,7 @@ class TestOCSF: "vendor_name": "Prowler", "version": prowler_version, }, - "version": "1.3.0", + "version": "1.4.0", "profiles": ["cloud", "datetime"], "tenant_uid": "test-organization-id", }, From 2934752eebade0285af9146f9f465470ff29dba4 Mon Sep 17 00:00:00 2001 From: Hugo Pereira Brito <101209179+HugoPBrito@users.noreply.github.com> Date: Tue, 4 Feb 2025 20:28:31 +0100 Subject: [PATCH 02/30] fix(elasticache): InvalidReplicationGroupStateFault error (#6815) Co-authored-by: MrCloudSec --- .../aws/services/elasticache/elasticache_service.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/prowler/providers/aws/services/elasticache/elasticache_service.py b/prowler/providers/aws/services/elasticache/elasticache_service.py index 5fc295630e..6e11a85bc6 100644 --- a/prowler/providers/aws/services/elasticache/elasticache_service.py +++ b/prowler/providers/aws/services/elasticache/elasticache_service.py @@ -147,6 +147,12 @@ class ElastiCache(AWSService): logger.warning( f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) + except ( + regional_client.exceptions.InvalidReplicationGroupStateFault + ) as error: + logger.warning( + f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) except Exception as error: logger.error( f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" @@ -163,6 +169,12 @@ class ElastiCache(AWSService): logger.warning( f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) + except ( + regional_client.exceptions.InvalidReplicationGroupStateFault + ) as error: + logger.warning( + f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) except Exception as error: logger.error( f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" From 9447b33800956e0d758fcfc41445d382cfd65168 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Feb 2025 17:22:51 -0500 Subject: [PATCH 03/30] chore(deps): bump kubernetes from 31.0.0 to 32.0.0 (#6678) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- poetry.lock | 15 +++++++++++---- pyproject.toml | 2 +- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/poetry.lock b/poetry.lock index 93e5fc2cfe..0f53a98c43 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2050,6 +2050,8 @@ optional = false python-versions = "*" files = [ {file = "jsonpath-ng-1.7.0.tar.gz", hash = "sha256:f6f5f7fd4e5ff79c785f1573b394043b39849fb2bb47bcead935d12b00beab3c"}, + {file = "jsonpath_ng-1.7.0-py2-none-any.whl", hash = "sha256:898c93fc173f0c336784a3fa63d7434297544b7198124a68f9a3ef9597b0ae6e"}, + {file = "jsonpath_ng-1.7.0-py3-none-any.whl", hash = "sha256:f3d7f9e848cba1b6da28c55b1c26ff915dc9e0b1ba7e752a53d6da8d5cbd00b6"}, ] [package.dependencies] @@ -2120,13 +2122,13 @@ referencing = ">=0.31.0" [[package]] name = "kubernetes" -version = "31.0.0" +version = "32.0.0" description = "Kubernetes python client" optional = false python-versions = ">=3.6" files = [ - {file = "kubernetes-31.0.0-py2.py3-none-any.whl", hash = "sha256:bf141e2d380c8520eada8b351f4e319ffee9636328c137aa432bc486ca1200e1"}, - {file = "kubernetes-31.0.0.tar.gz", hash = "sha256:28945de906c8c259c1ebe62703b56a03b714049372196f854105afe4e6d014c0"}, + {file = "kubernetes-32.0.0-py2.py3-none-any.whl", hash = "sha256:60fd8c29e8e43d9c553ca4811895a687426717deba9c0a66fb2dcc3f5ef96692"}, + {file = "kubernetes-32.0.0.tar.gz", hash = "sha256:319fa840345a482001ac5d6062222daeb66ec4d1bcb3087402aed685adf0aecb"}, ] [package.dependencies] @@ -4308,6 +4310,7 @@ files = [ {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f66efbc1caa63c088dead1c4170d148eabc9b80d95fb75b6c92ac0aad2437d76"}, {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:22353049ba4181685023b25b5b51a574bce33e7f51c759371a7422dcae5402a6"}, {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:932205970b9f9991b34f55136be327501903f7c66830e9760a8ffb15b07f05cd"}, + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a52d48f4e7bf9005e8f0a89209bf9a73f7190ddf0489eee5eb51377385f59f2a"}, {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win32.whl", hash = "sha256:3eac5a91891ceb88138c113f9db04f3cebdae277f5d44eaa3651a4f573e6a5da"}, {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win_amd64.whl", hash = "sha256:ab007f2f5a87bd08ab1499bdf96f3d5c6ad4dcfa364884cb4549aa0154b13a28"}, {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:4a6679521a58256a90b0d89e03992c15144c5f3858f40d7c18886023d7943db6"}, @@ -4316,6 +4319,7 @@ files = [ {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:811ea1594b8a0fb466172c384267a4e5e367298af6b228931f273b111f17ef52"}, {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cf12567a7b565cbf65d438dec6cfbe2917d3c1bdddfce84a9930b7d35ea59642"}, {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7dd5adc8b930b12c8fc5b99e2d535a09889941aa0d0bd06f4749e9a9397c71d2"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1492a6051dab8d912fc2adeef0e8c72216b24d57bd896ea607cb90bb0c4981d3"}, {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win32.whl", hash = "sha256:bd0a08f0bab19093c54e18a14a10b4322e1eacc5217056f3c063bd2f59853ce4"}, {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win_amd64.whl", hash = "sha256:a274fb2cb086c7a3dea4322ec27f4cb5cc4b6298adb583ab0e211a4682f241eb"}, {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:20b0f8dc160ba83b6dcc0e256846e1a02d044e13f7ea74a3d1d56ede4e48c632"}, @@ -4324,6 +4328,7 @@ files = [ {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:749c16fcc4a2b09f28843cda5a193e0283e47454b63ec4b81eaa2242f50e4ccd"}, {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bf165fef1f223beae7333275156ab2022cffe255dcc51c27f066b4370da81e31"}, {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:32621c177bbf782ca5a18ba4d7af0f1082a3f6e517ac2a18b3974d4edf349680"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b82a7c94a498853aa0b272fd5bc67f29008da798d4f93a2f9f289feb8426a58d"}, {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win32.whl", hash = "sha256:e8c4ebfcfd57177b572e2040777b8abc537cdef58a2120e830124946aa9b42c5"}, {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win_amd64.whl", hash = "sha256:0467c5965282c62203273b838ae77c0d29d7638c8a4e3a1c8bdd3602c10904e4"}, {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4c8c5d82f50bb53986a5e02d1b3092b03622c02c2eb78e29bec33fd9593bae1a"}, @@ -4332,6 +4337,7 @@ files = [ {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96777d473c05ee3e5e3c3e999f5d23c6f4ec5b0c38c098b3a5229085f74236c6"}, {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:3bc2a80e6420ca8b7d3590791e2dfc709c88ab9152c00eeb511c9875ce5778bf"}, {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e188d2699864c11c36cdfdada94d781fd5d6b0071cd9c427bceb08ad3d7c70e1"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f6f3eac23941b32afccc23081e1f50612bdbe4e982012ef4f5797986828cd01"}, {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win32.whl", hash = "sha256:6442cb36270b3afb1b4951f060eccca1ce49f3d087ca1ca4563a6eb479cb3de6"}, {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win_amd64.whl", hash = "sha256:e5b8daf27af0b90da7bb903a876477a9e6d7270be6146906b276605997c7e9a3"}, {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:fc4b630cd3fa2cf7fce38afa91d7cfe844a9f75d7f0f36393fa98815e911d987"}, @@ -4340,6 +4346,7 @@ files = [ {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2f1c3765db32be59d18ab3953f43ab62a761327aafc1594a2a1fbe038b8b8a7"}, {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d85252669dc32f98ebcd5d36768f5d4faeaeaa2d655ac0473be490ecdae3c285"}, {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e143ada795c341b56de9418c58d028989093ee611aa27ffb9b7f609c00d813ed"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2c59aa6170b990d8d2719323e628aaf36f3bfbc1c26279c0eeeb24d05d2d11c7"}, {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win32.whl", hash = "sha256:beffaed67936fbbeffd10966a4eb53c402fafd3d6833770516bf7314bc6ffa12"}, {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win_amd64.whl", hash = "sha256:040ae85536960525ea62868b642bdb0c2cc6021c9f9d507810c0c604e66f5a7b"}, {file = "ruamel.yaml.clib-0.2.12.tar.gz", hash = "sha256:6c8fbb13ec503f99a91901ab46e0b07ae7941cd527393187039aec586fdfd36f"}, @@ -5083,4 +5090,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.13" -content-hash = "d4d817d0a3690b7a39222bf79420e3adb764b47c6c1d2ee0c73a7599333114b1" +content-hash = "7511c2a5abad01f351b1d222d73a6b2c5374f386e8c035cce79934daca7ea762" diff --git a/pyproject.toml b/pyproject.toml index 1fb1ab82a2..b9cb7f8b1f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,7 +58,7 @@ detect-secrets = "1.5.0" google-api-python-client = "2.160.0" google-auth-httplib2 = ">=0.1,<0.3" jsonschema = "4.23.0" -kubernetes = "31.0.0" +kubernetes = "32.0.0" microsoft-kiota-abstractions = "1.9.1" msgraph-sdk = "1.18.0" numpy = "2.0.2" From 43c13b7ba14d39f67ca7a5df4522ed6d756e4984 Mon Sep 17 00:00:00 2001 From: Mario Rodriguez Lopez <101330800+MarioRgzLpz@users.noreply.github.com> Date: Tue, 4 Feb 2025 23:24:11 +0100 Subject: [PATCH 04/30] feat(microsoft365): add new check `admincenter_settings_password_never_expire` (#6023) Co-authored-by: MrCloudSec --- .../{microsoft365_regions.py => regions.py} | 0 .../microsoft365/microsoft365_provider.py | 4 +- .../admincenter/admincenter_service.py | 29 +++++ .../__init__.py | 0 ...ttings_password_never_expire.metadata.json | 30 +++++ ...incenter_settings_password_never_expire.py | 27 +++++ ...sers_between_two_and_four_global_admins.py | 4 +- .../lib/regions/microsoft365_regions_test.py | 2 +- .../microsoft365/microsoft365_fixtures.py | 1 - .../microsoft365_provider_test.py | 2 - ...ter_settings_password_never_expire_test.py | 112 ++++++++++++++++++ 11 files changed, 201 insertions(+), 10 deletions(-) rename prowler/providers/microsoft365/lib/regions/{microsoft365_regions.py => regions.py} (100%) create mode 100644 prowler/providers/microsoft365/services/admincenter/admincenter_settings_password_never_expire/__init__.py create mode 100644 prowler/providers/microsoft365/services/admincenter/admincenter_settings_password_never_expire/admincenter_settings_password_never_expire.metadata.json create mode 100644 prowler/providers/microsoft365/services/admincenter/admincenter_settings_password_never_expire/admincenter_settings_password_never_expire.py create mode 100644 tests/providers/microsoft365/services/admincenter/admincenter_settings_password_never_expire/admincenter_settings_password_never_expire_test.py diff --git a/prowler/providers/microsoft365/lib/regions/microsoft365_regions.py b/prowler/providers/microsoft365/lib/regions/regions.py similarity index 100% rename from prowler/providers/microsoft365/lib/regions/microsoft365_regions.py rename to prowler/providers/microsoft365/lib/regions/regions.py diff --git a/prowler/providers/microsoft365/microsoft365_provider.py b/prowler/providers/microsoft365/microsoft365_provider.py index dff8da4d97..af7a723f67 100644 --- a/prowler/providers/microsoft365/microsoft365_provider.py +++ b/prowler/providers/microsoft365/microsoft365_provider.py @@ -50,9 +50,7 @@ from prowler.providers.microsoft365.exceptions.exceptions import ( Microsoft365TenantIdAndClientSecretNotBelongingToClientIdError, ) from prowler.providers.microsoft365.lib.mutelist.mutelist import Microsoft365Mutelist -from prowler.providers.microsoft365.lib.regions.microsoft365_regions import ( - get_regions_config, -) +from prowler.providers.microsoft365.lib.regions.regions import get_regions_config from prowler.providers.microsoft365.models import ( Microsoft365IdentityInfo, Microsoft365RegionConfig, diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_service.py b/prowler/providers/microsoft365/services/admincenter/admincenter_service.py index 2fc14d1ae6..fed450ca82 100644 --- a/prowler/providers/microsoft365/services/admincenter/admincenter_service.py +++ b/prowler/providers/microsoft365/services/admincenter/admincenter_service.py @@ -22,11 +22,13 @@ class AdminCenter(Microsoft365Service): gather( self._get_directory_roles(), self._get_groups(), + self._get_domains(), ) ) self.directory_roles = attributes[0] self.groups = attributes[1] + self.domains = attributes[2] async def _get_users(self): logger.info("Microsoft365 - Getting users...") @@ -130,6 +132,28 @@ class AdminCenter(Microsoft365Service): ) return groups + async def _get_domains(self): + logger.info("Microsoft365 - Getting domains...") + domains = {} + try: + domains_list = await self.client.domains.get() + domains.update({}) + for domain in domains_list.value: + domains.update( + { + domain.id: Domain( + id=domain.id, + password_validity_period=domain.password_validity_period_in_days, + ) + } + ) + + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + return domains + class User(BaseModel): id: str @@ -149,3 +173,8 @@ class Group(BaseModel): id: str name: str visibility: str + + +class Domain(BaseModel): + id: str + password_validity_period: int diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_settings_password_never_expire/__init__.py b/prowler/providers/microsoft365/services/admincenter/admincenter_settings_password_never_expire/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_settings_password_never_expire/admincenter_settings_password_never_expire.metadata.json b/prowler/providers/microsoft365/services/admincenter/admincenter_settings_password_never_expire/admincenter_settings_password_never_expire.metadata.json new file mode 100644 index 0000000000..13528b0008 --- /dev/null +++ b/prowler/providers/microsoft365/services/admincenter/admincenter_settings_password_never_expire/admincenter_settings_password_never_expire.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "microsoft", + "CheckID": "admincenter_settings_password_never_expire", + "CheckTitle": "Ensure the 'Password expiration policy' is set to 'Set passwords to never expire (recommended)'", + "CheckType": [], + "ServiceName": "admincenter", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "Microsoft365Domain", + "Description": "This control ensures that the password expiration policy is set to 'Set passwords to never expire (recommended)'. This aligns with modern recommendations to enhance security by avoiding arbitrary password changes and focusing on supplementary controls like MFA.", + "Risk": "Arbitrary password expiration policies can lead to weaker passwords due to frequent changes. Users may adopt insecure habits such as using simple, memorable passwords.", + "RelatedUrl": "https://www.cisecurity.org/insights/white-papers/cis-password-policy-guide", + "Remediation": { + "Code": { + "CLI": "Set-MsolUser -UserPrincipalName -PasswordNeverExpires $true", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Enable the 'Never Expire Passwords' option in Microsoft 365 Admin Center.", + "Url": "https://learn.microsoft.com/en-us/microsoft-365/admin/misc/password-policy-recommendations?view=o365-worldwide" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_settings_password_never_expire/admincenter_settings_password_never_expire.py b/prowler/providers/microsoft365/services/admincenter/admincenter_settings_password_never_expire/admincenter_settings_password_never_expire.py new file mode 100644 index 0000000000..8de62dd8d5 --- /dev/null +++ b/prowler/providers/microsoft365/services/admincenter/admincenter_settings_password_never_expire/admincenter_settings_password_never_expire.py @@ -0,0 +1,27 @@ +from prowler.lib.check.models import Check, Check_Report_Microsoft365 +from prowler.providers.microsoft365.services.admincenter.admincenter_client import ( + admincenter_client, +) + + +class admincenter_settings_password_never_expire(Check): + def execute(self) -> Check_Report_Microsoft365: + findings = [] + for domain in admincenter_client.domains.values(): + report = Check_Report_Microsoft365(self.metadata(), resource=domain) + report.resource_id = domain.id + report.resource_name = domain.id + report.status = "FAIL" + report.status_extended = ( + f"Domain {domain.id} does not have a Password never expires policy." + ) + + if domain.password_validity_period == 2147483647: + report.status = "PASS" + report.status_extended = ( + f"Domain {domain.id} Password policy is set to never expire." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins.py b/prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins.py index aa0fc978c0..6002e67e4c 100644 --- a/prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins.py +++ b/prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins.py @@ -9,9 +9,7 @@ class admincenter_users_between_two_and_four_global_admins(Check): findings = [] directory_roles = admincenter_client.directory_roles - report = Check_Report_Microsoft365( - metadata=self.metadata(), resource=admincenter_client.directory_roles - ) + report = Check_Report_Microsoft365(metadata=self.metadata(), resource={}) report.status = "FAIL" report.resource_name = "Global Administrator" diff --git a/tests/providers/microsoft365/lib/regions/microsoft365_regions_test.py b/tests/providers/microsoft365/lib/regions/microsoft365_regions_test.py index 3e358511b8..49ec5a480d 100644 --- a/tests/providers/microsoft365/lib/regions/microsoft365_regions_test.py +++ b/tests/providers/microsoft365/lib/regions/microsoft365_regions_test.py @@ -1,6 +1,6 @@ from azure.identity import AzureAuthorityHosts -from prowler.providers.microsoft365.lib.regions.microsoft365_regions import ( +from prowler.providers.microsoft365.lib.regions.regions import ( MICROSOFT365_CHINA_CLOUD, MICROSOFT365_GENERIC_CLOUD, MICROSOFT365_US_GOV_CLOUD, diff --git a/tests/providers/microsoft365/microsoft365_fixtures.py b/tests/providers/microsoft365/microsoft365_fixtures.py index 79e8ec118f..1dbb09c612 100644 --- a/tests/providers/microsoft365/microsoft365_fixtures.py +++ b/tests/providers/microsoft365/microsoft365_fixtures.py @@ -29,7 +29,6 @@ def set_mocked_microsoft365_provider( audit_config: dict = None, azure_region_config: Microsoft365RegionConfig = Microsoft365RegionConfig(), ) -> Microsoft365Provider: - provider = MagicMock() provider.type = "microsoft365" provider.session.credentials = credentials diff --git a/tests/providers/microsoft365/microsoft365_provider_test.py b/tests/providers/microsoft365/microsoft365_provider_test.py index b475a02ca6..7658301e44 100644 --- a/tests/providers/microsoft365/microsoft365_provider_test.py +++ b/tests/providers/microsoft365/microsoft365_provider_test.py @@ -212,7 +212,6 @@ class TestMicrosoft365Provider: "prowler.providers.microsoft365.microsoft365_provider.GraphServiceClient" ) as mock_graph_client, ): - # Mock the return value of DefaultAzureCredential mock_credentials = MagicMock() mock_credentials.get_token.return_value = AccessToken( @@ -271,7 +270,6 @@ class TestMicrosoft365Provider: with patch( "prowler.providers.microsoft365.microsoft365_provider.Microsoft365Provider.setup_session" ) as mock_setup_session: - mock_setup_session.side_effect = Microsoft365HTTPResponseError( file="test_file", original_exception="Simulated HttpResponseError" ) diff --git a/tests/providers/microsoft365/services/admincenter/admincenter_settings_password_never_expire/admincenter_settings_password_never_expire_test.py b/tests/providers/microsoft365/services/admincenter/admincenter_settings_password_never_expire/admincenter_settings_password_never_expire_test.py new file mode 100644 index 0000000000..b49bc98095 --- /dev/null +++ b/tests/providers/microsoft365/services/admincenter/admincenter_settings_password_never_expire/admincenter_settings_password_never_expire_test.py @@ -0,0 +1,112 @@ +from unittest import mock +from uuid import uuid4 + +from tests.providers.microsoft365.microsoft365_fixtures import ( + DOMAIN, + set_mocked_microsoft365_provider, +) + + +class Test_admincenter_settings_password_never_expire: + def test_admincenter_no_domains(self): + admincenter_client = mock.MagicMock + admincenter_client.audited_tenant = "audited_tenant" + admincenter_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_microsoft365_provider(), + ), + mock.patch( + "prowler.providers.microsoft365.services.admincenter.admincenter_settings_password_never_expire.admincenter_settings_password_never_expire.admincenter_client", + new=admincenter_client, + ), + ): + from prowler.providers.microsoft365.services.admincenter.admincenter_settings_password_never_expire.admincenter_settings_password_never_expire import ( + admincenter_settings_password_never_expire, + ) + + admincenter_client.domains = {} + + check = admincenter_settings_password_never_expire() + result = check.execute() + assert len(result) == 0 + + def test_admincenter_domain_password_expire(self): + admincenter_client = mock.MagicMock + admincenter_client.audited_tenant = "audited_tenant" + admincenter_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_microsoft365_provider(), + ), + mock.patch( + "prowler.providers.microsoft365.services.admincenter.admincenter_settings_password_never_expire.admincenter_settings_password_never_expire.admincenter_client", + new=admincenter_client, + ), + ): + from prowler.providers.microsoft365.services.admincenter.admincenter_service import ( + Domain, + ) + from prowler.providers.microsoft365.services.admincenter.admincenter_settings_password_never_expire.admincenter_settings_password_never_expire import ( + admincenter_settings_password_never_expire, + ) + + id_domain = str(uuid4()) + + admincenter_client.domains = { + id_domain: Domain(id=id_domain, password_validity_period=5), + } + + check = admincenter_settings_password_never_expire() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Domain {id_domain} does not have a Password never expires policy." + ) + assert result[0].resource_name == id_domain + assert result[0].resource_id == id_domain + + def test_admincenter_password_not_expire(self): + admincenter_client = mock.MagicMock + admincenter_client.audited_tenant = "audited_tenant" + admincenter_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_microsoft365_provider(), + ), + mock.patch( + "prowler.providers.microsoft365.services.admincenter.admincenter_settings_password_never_expire.admincenter_settings_password_never_expire.admincenter_client", + new=admincenter_client, + ), + ): + from prowler.providers.microsoft365.services.admincenter.admincenter_service import ( + Domain, + ) + from prowler.providers.microsoft365.services.admincenter.admincenter_settings_password_never_expire.admincenter_settings_password_never_expire import ( + admincenter_settings_password_never_expire, + ) + + id_domain = str(uuid4()) + + admincenter_client.domains = { + id_domain: Domain(id=id_domain, password_validity_period=2147483647), + } + + check = admincenter_settings_password_never_expire() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Domain {id_domain} Password policy is set to never expire." + ) + assert result[0].resource_name == id_domain + assert result[0].resource_id == id_domain From f3b121950d5d03e028158cb8080069802dfba260 Mon Sep 17 00:00:00 2001 From: Mario Rodriguez Lopez <101330800+MarioRgzLpz@users.noreply.github.com> Date: Wed, 5 Feb 2025 01:47:14 +0100 Subject: [PATCH 05/30] feat(entra): add new `entra` service for Microsoft365 (#6326) Co-authored-by: MrCloudSec --- .../microsoft365/services/entra/__init__.py | 0 .../services/entra/entra_client.py | 4 + .../services/entra/entra_service.py | 105 ++++++++++++++++++ .../entra/microsoft365_entra_service_test.py | 45 ++++++++ 4 files changed, 154 insertions(+) create mode 100644 prowler/providers/microsoft365/services/entra/__init__.py create mode 100644 prowler/providers/microsoft365/services/entra/entra_client.py create mode 100644 prowler/providers/microsoft365/services/entra/entra_service.py create mode 100644 tests/providers/microsoft365/services/entra/microsoft365_entra_service_test.py diff --git a/prowler/providers/microsoft365/services/entra/__init__.py b/prowler/providers/microsoft365/services/entra/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/microsoft365/services/entra/entra_client.py b/prowler/providers/microsoft365/services/entra/entra_client.py new file mode 100644 index 0000000000..1a3b921adf --- /dev/null +++ b/prowler/providers/microsoft365/services/entra/entra_client.py @@ -0,0 +1,4 @@ +from prowler.providers.common.provider import Provider +from prowler.providers.microsoft365.services.entra.entra_service import Entra + +entra_client = Entra(Provider.get_global_provider()) diff --git a/prowler/providers/microsoft365/services/entra/entra_service.py b/prowler/providers/microsoft365/services/entra/entra_service.py new file mode 100644 index 0000000000..8561a31edd --- /dev/null +++ b/prowler/providers/microsoft365/services/entra/entra_service.py @@ -0,0 +1,105 @@ +from asyncio import gather, get_event_loop +from typing import List, Optional + +from pydantic import BaseModel + +from prowler.lib.logger import logger +from prowler.providers.microsoft365.lib.service.service import Microsoft365Service +from prowler.providers.microsoft365.microsoft365_provider import Microsoft365Provider + + +class Entra(Microsoft365Service): + def __init__(self, provider: Microsoft365Provider): + super().__init__(provider) + + loop = get_event_loop() + + attributes = loop.run_until_complete( + gather( + self._get_authorization_policy(), + ) + ) + + self.authorization_policy = attributes[0] + + async def _get_authorization_policy(self): + logger.info("Entra - Getting authorization policy...") + + authorization_policy = {} + try: + auth_policy = await self.client.policies.authorization_policy.get() + + default_user_role_permissions = getattr( + auth_policy, "default_user_role_permissions", None + ) + + authorization_policy.update( + { + auth_policy.id: AuthorizationPolicy( + id=auth_policy.id, + name=auth_policy.display_name, + description=auth_policy.description, + default_user_role_permissions=DefaultUserRolePermissions( + allowed_to_create_apps=getattr( + default_user_role_permissions, + "allowed_to_create_apps", + None, + ), + allowed_to_create_security_groups=getattr( + default_user_role_permissions, + "allowed_to_create_security_groups", + None, + ), + allowed_to_create_tenants=getattr( + default_user_role_permissions, + "allowed_to_create_tenants", + None, + ), + allowed_to_read_bitlocker_keys_for_owned_device=getattr( + default_user_role_permissions, + "allowed_to_read_bitlocker_keys_for_owned_device", + None, + ), + allowed_to_read_other_users=getattr( + default_user_role_permissions, + "allowed_to_read_other_users", + None, + ), + odata_type=getattr( + default_user_role_permissions, "odata_type", None + ), + permission_grant_policies_assigned=[ + policy_assigned + for policy_assigned in getattr( + default_user_role_permissions, + "permission_grant_policies_assigned", + [], + ) + ], + ), + ) + } + ) + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + + return authorization_policy + + +class DefaultUserRolePermissions(BaseModel): + allowed_to_create_apps: Optional[bool] + allowed_to_create_security_groups: Optional[bool] + allowed_to_create_tenants: Optional[bool] + allowed_to_read_bitlocker_keys_for_owned_device: Optional[bool] + allowed_to_read_other_users: Optional[bool] + odata_type: Optional[str] + permission_grant_policies_assigned: Optional[List[str]] = None + + +class AuthorizationPolicy(BaseModel): + id: str + name: str + description: str + default_user_role_permissions: Optional[DefaultUserRolePermissions] diff --git a/tests/providers/microsoft365/services/entra/microsoft365_entra_service_test.py b/tests/providers/microsoft365/services/entra/microsoft365_entra_service_test.py new file mode 100644 index 0000000000..86109478e2 --- /dev/null +++ b/tests/providers/microsoft365/services/entra/microsoft365_entra_service_test.py @@ -0,0 +1,45 @@ +from unittest.mock import patch + +from prowler.providers.microsoft365.models import Microsoft365IdentityInfo +from prowler.providers.microsoft365.services.entra.entra_service import ( + AuthorizationPolicy, + Entra, +) +from tests.providers.microsoft365.microsoft365_fixtures import ( + DOMAIN, + set_mocked_microsoft365_provider, +) + + +async def mock_entra_get_authorization_policy(_): + return { + "id-1": AuthorizationPolicy( + id="id-1", + name="Name 1", + description="Description 1", + default_user_role_permissions=None, + ) + } + + +@patch( + "prowler.providers.microsoft365.services.entra.entra_service.Entra._get_authorization_policy", + new=mock_entra_get_authorization_policy, +) +class Test_Entra_Service: + def test_get_client(self): + admincenter_client = Entra( + set_mocked_microsoft365_provider( + identity=Microsoft365IdentityInfo(tenant_domain=DOMAIN) + ) + ) + assert admincenter_client.client.__class__.__name__ == "GraphServiceClient" + + def test_get_authorization_policy(self): + entra_client = Entra(set_mocked_microsoft365_provider()) + assert entra_client.authorization_policy["id-1"].id == "id-1" + assert entra_client.authorization_policy["id-1"].name == "Name 1" + assert entra_client.authorization_policy["id-1"].description == "Description 1" + assert not entra_client.authorization_policy[ + "id-1" + ].default_user_role_permissions From 3a149fa45968343c7078dc7455b906a8ab78c44e Mon Sep 17 00:00:00 2001 From: Prowler Bot Date: Wed, 5 Feb 2025 15:19:56 +0100 Subject: [PATCH 06/30] chore(regions_update): Changes in regions for AWS services (#6821) Co-authored-by: MrCloudSec <38561120+MrCloudSec@users.noreply.github.com> --- prowler/providers/aws/aws_regions_by_service.json | 1 + 1 file changed, 1 insertion(+) diff --git a/prowler/providers/aws/aws_regions_by_service.json b/prowler/providers/aws/aws_regions_by_service.json index a589f266df..d9ed6b445c 100644 --- a/prowler/providers/aws/aws_regions_by_service.json +++ b/prowler/providers/aws/aws_regions_by_service.json @@ -6782,6 +6782,7 @@ "mcs": { "regions": { "aws": [ + "af-south-1", "ap-east-1", "ap-northeast-1", "ap-northeast-2", From 1c778354da14b4e919727af9e0b1409acd63eda6 Mon Sep 17 00:00:00 2001 From: Daniel Barranquero <74871504+danibarranqueroo@users.noreply.github.com> Date: Wed, 5 Feb 2025 16:22:32 +0100 Subject: [PATCH 07/30] fix(directoryservice): handle ClientException (#6781) Co-authored-by: MrCloudSec --- .../directoryservice_service.py | 59 +++++++++++++++---- 1 file changed, 46 insertions(+), 13 deletions(-) diff --git a/prowler/providers/aws/services/directoryservice/directoryservice_service.py b/prowler/providers/aws/services/directoryservice/directoryservice_service.py index 8df5b1918e..0beea818e2 100644 --- a/prowler/providers/aws/services/directoryservice/directoryservice_service.py +++ b/prowler/providers/aws/services/directoryservice/directoryservice_service.py @@ -107,21 +107,45 @@ class DirectoryService(AWSService): if directory.region == regional_client.region: # Operation is not supported for Shared MicrosoftAD directories. if directory.type != DirectoryType.SharedMicrosoftAD: - describe_event_topics_parameters = {"DirectoryId": directory.id} - event_topics = [] - describe_event_topics = regional_client.describe_event_topics( - **describe_event_topics_parameters - ) - for event_topic in describe_event_topics["EventTopics"]: - event_topics.append( - EventTopics( - topic_arn=event_topic["TopicArn"], - topic_name=event_topic["TopicName"], - status=event_topic["Status"], - created_date_time=event_topic["CreatedDateTime"], + try: + describe_event_topics_parameters = { + "DirectoryId": directory.id + } + event_topics = [] + describe_event_topics = ( + regional_client.describe_event_topics( + **describe_event_topics_parameters ) ) - self.directories[directory.id].event_topics = event_topics + for event_topic in describe_event_topics["EventTopics"]: + event_topics.append( + EventTopics( + topic_arn=event_topic["TopicArn"], + topic_name=event_topic["TopicName"], + status=event_topic["Status"], + created_date_time=event_topic[ + "CreatedDateTime" + ], + ) + ) + self.directories[directory.id].event_topics = event_topics + except ClientError as error: + if ( + "is in Deleting state" + in error.response["Error"]["Message"] + ): + logger.warning( + f"{directory.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + else: + logger.error( + f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + except Exception as error: + logger.error( + f"{regional_client.region} -- {error.__class__.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + except Exception as error: logger.error( f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" @@ -202,6 +226,15 @@ class DirectoryService(AWSService): "SnapshotLimits" ]["ManualSnapshotsLimitReached"], ) + except ClientError as error: + if "is in Deleting state" in error.response["Error"]["Message"]: + logger.warning( + f"{directory.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + else: + logger.error( + f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) except Exception as error: logger.error( f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" From 3f03dd20e4c85551d8ffeede9e199c7061ba8af4 Mon Sep 17 00:00:00 2001 From: Kay Agahd Date: Wed, 5 Feb 2025 16:23:52 +0100 Subject: [PATCH 08/30] fix(aws) wording of report.status_extended in awslambda_function_not_publicly_accessible (#6824) --- ...awslambda_function_not_publicly_accessible.py | 4 ++-- ...mbda_function_not_publicly_accessible_test.py | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/prowler/providers/aws/services/awslambda/awslambda_function_not_publicly_accessible/awslambda_function_not_publicly_accessible.py b/prowler/providers/aws/services/awslambda/awslambda_function_not_publicly_accessible/awslambda_function_not_publicly_accessible.py index 4ab57e0de8..86c3cbfec8 100644 --- a/prowler/providers/aws/services/awslambda/awslambda_function_not_publicly_accessible/awslambda_function_not_publicly_accessible.py +++ b/prowler/providers/aws/services/awslambda/awslambda_function_not_publicly_accessible/awslambda_function_not_publicly_accessible.py @@ -10,14 +10,14 @@ class awslambda_function_not_publicly_accessible(Check): report = Check_Report_AWS(metadata=self.metadata(), resource=function) report.status = "PASS" - report.status_extended = f"Lambda function {function.name} has a policy resource-based policy not public." + report.status_extended = f"Lambda function {function.name} has a resource-based policy without public access." if is_policy_public( function.policy, awslambda_client.audited_account, is_cross_account_allowed=True, ): report.status = "FAIL" - report.status_extended = f"Lambda function {function.name} has a policy resource-based policy with public access." + report.status_extended = f"Lambda function {function.name} has a resource-based policy with public access." findings.append(report) diff --git a/tests/providers/aws/services/awslambda/awslambda_function_not_publicly_accessible/awslambda_function_not_publicly_accessible_test.py b/tests/providers/aws/services/awslambda/awslambda_function_not_publicly_accessible/awslambda_function_not_publicly_accessible_test.py index d7f8d75a40..99aaec831a 100644 --- a/tests/providers/aws/services/awslambda/awslambda_function_not_publicly_accessible/awslambda_function_not_publicly_accessible_test.py +++ b/tests/providers/aws/services/awslambda/awslambda_function_not_publicly_accessible/awslambda_function_not_publicly_accessible_test.py @@ -107,7 +107,7 @@ class Test_awslambda_function_not_publicly_accessible: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Lambda function {function_name} has a policy resource-based policy with public access." + == f"Lambda function {function_name} has a resource-based policy with public access." ) assert result[0].resource_tags == [{"tag1": "value1", "tag2": "value2"}] @@ -184,7 +184,7 @@ class Test_awslambda_function_not_publicly_accessible: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Lambda function {function_name} has a policy resource-based policy not public." + == f"Lambda function {function_name} has a resource-based policy without public access." ) assert result[0].resource_tags == [{"tag1": "value1", "tag2": "value2"}] @@ -260,7 +260,7 @@ class Test_awslambda_function_not_publicly_accessible: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Lambda function {function_name} has a policy resource-based policy not public." + == f"Lambda function {function_name} has a resource-based policy without public access." ) assert result[0].resource_tags == [{"tag1": "value1", "tag2": "value2"}] @@ -319,7 +319,7 @@ class Test_awslambda_function_not_publicly_accessible: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Lambda function {function_name} has a policy resource-based policy with public access." + == f"Lambda function {function_name} has a resource-based policy with public access." ) assert result[0].resource_tags == [] @@ -492,7 +492,7 @@ class Test_awslambda_function_not_publicly_accessible: assert result[0].status == "FAIL" assert ( result[0].status_extended - == "Lambda function test-public-lambda has a policy resource-based policy with public access." + == "Lambda function test-public-lambda has a resource-based policy with public access." ) assert result[0].resource_tags == [{"tag1": "value1", "tag2": "value2"}] @@ -552,7 +552,7 @@ class Test_awslambda_function_not_publicly_accessible: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Lambda function {function_name} has a policy resource-based policy not public." + == f"Lambda function {function_name} has a resource-based policy without public access." ) assert result[0].resource_tags == [] @@ -612,7 +612,7 @@ class Test_awslambda_function_not_publicly_accessible: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Lambda function {function_name} has a policy resource-based policy not public." + == f"Lambda function {function_name} has a resource-based policy without public access." ) assert result[0].resource_tags == [] @@ -681,7 +681,7 @@ class Test_awslambda_function_not_publicly_accessible: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Lambda function {function_name} has a policy resource-based policy with public access." + == f"Lambda function {function_name} has a resource-based policy with public access." ) assert result[0].resource_id == function_name assert result[0].resource_arn == function_arn From 567c729e9e111b206a5073f4968d96c557b3c260 Mon Sep 17 00:00:00 2001 From: Gary Mclean Date: Wed, 5 Feb 2025 15:26:50 +0000 Subject: [PATCH 09/30] fix(findings) Spelling mistakes correction (#6822) --- .../entra_security_defaults_enabled.py | 2 +- .../entra_security_defaults_enabled_test.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/prowler/providers/azure/services/entra/entra_security_defaults_enabled/entra_security_defaults_enabled.py b/prowler/providers/azure/services/entra/entra_security_defaults_enabled/entra_security_defaults_enabled.py index b7deed387b..3a7e1f1bc5 100644 --- a/prowler/providers/azure/services/entra/entra_security_defaults_enabled/entra_security_defaults_enabled.py +++ b/prowler/providers/azure/services/entra/entra_security_defaults_enabled/entra_security_defaults_enabled.py @@ -15,7 +15,7 @@ class entra_security_defaults_enabled(Check): ) report.subscription = f"Tenant: {tenant}" report.status = "FAIL" - report.status_extended = "Entra security defaults is diabled." + report.status_extended = "Entra security defaults is disabled." if getattr(security_default, "is_enabled", False): report.status = "PASS" diff --git a/tests/providers/azure/services/entra/entra_security_defaults_enabled/entra_security_defaults_enabled_test.py b/tests/providers/azure/services/entra/entra_security_defaults_enabled/entra_security_defaults_enabled_test.py index b1887a7425..33f3f5a839 100644 --- a/tests/providers/azure/services/entra/entra_security_defaults_enabled/entra_security_defaults_enabled_test.py +++ b/tests/providers/azure/services/entra/entra_security_defaults_enabled/entra_security_defaults_enabled_test.py @@ -45,7 +45,7 @@ class Test_entra_security_defaults_enabled: result = check.execute() assert len(result) == 1 assert result[0].status == "FAIL" - assert result[0].status_extended == "Entra security defaults is diabled." + assert result[0].status_extended == "Entra security defaults is disabled." assert result[0].subscription == f"Tenant: {DOMAIN}" assert result[0].resource_name == "" assert result[0].resource_id == "" @@ -109,7 +109,7 @@ class Test_entra_security_defaults_enabled: result = check.execute() assert len(result) == 1 assert result[0].status == "FAIL" - assert result[0].status_extended == "Entra security defaults is diabled." + assert result[0].status_extended == "Entra security defaults is disabled." assert result[0].subscription == f"Tenant: {DOMAIN}" assert result[0].resource_name == "Sec Default" assert result[0].resource_id == id From 8d1c476aedef23455bbb9f4f0963b926753fd181 Mon Sep 17 00:00:00 2001 From: Ogonna Iwunze <1915636+wunzeco@users.noreply.github.com> Date: Wed, 5 Feb 2025 16:20:29 +0000 Subject: [PATCH 10/30] feat(kms): add `kms_cmk_not_multi_region` AWS check (#6794) Co-authored-by: MrCloudSec --- .../kms/kms_cmk_not_multi_region/__init__.py | 0 .../kms_cmk_not_multi_region.metadata.json | 32 ++++ .../kms_cmk_not_multi_region.py | 25 +++ .../providers/aws/services/kms/kms_service.py | 2 + .../kms/kms_cmk_not_multi_region/__init__.py | 0 .../kms_cmk_not_multi_region_test.py | 142 ++++++++++++++++++ .../aws/services/kms/kms_service_test.py | 16 +- 7 files changed, 212 insertions(+), 5 deletions(-) create mode 100644 prowler/providers/aws/services/kms/kms_cmk_not_multi_region/__init__.py create mode 100644 prowler/providers/aws/services/kms/kms_cmk_not_multi_region/kms_cmk_not_multi_region.metadata.json create mode 100644 prowler/providers/aws/services/kms/kms_cmk_not_multi_region/kms_cmk_not_multi_region.py create mode 100644 tests/providers/aws/services/kms/kms_cmk_not_multi_region/__init__.py create mode 100644 tests/providers/aws/services/kms/kms_cmk_not_multi_region/kms_cmk_not_multi_region_test.py diff --git a/prowler/providers/aws/services/kms/kms_cmk_not_multi_region/__init__.py b/prowler/providers/aws/services/kms/kms_cmk_not_multi_region/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/kms/kms_cmk_not_multi_region/kms_cmk_not_multi_region.metadata.json b/prowler/providers/aws/services/kms/kms_cmk_not_multi_region/kms_cmk_not_multi_region.metadata.json new file mode 100644 index 0000000000..c8b603893d --- /dev/null +++ b/prowler/providers/aws/services/kms/kms_cmk_not_multi_region/kms_cmk_not_multi_region.metadata.json @@ -0,0 +1,32 @@ +{ + "Provider": "aws", + "CheckID": "kms_cmk_not_multi_region", + "CheckTitle": "AWS KMS customer managed keys should not be multi-Region", + "CheckType": [ + "Data Protection" + ], + "ServiceName": "kms", + "SubServiceName": "", + "ResourceIdTemplate": "arn:partition:kms:region:account-id:key/resource-id", + "Severity": "high", + "ResourceType": "AwsKmsKey", + "Description": "Ensure that AWS KMS customer managed keys (CMKs) are not multi-region to maintain strict data control and compliance with security best practices.", + "Risk": "Multi-region KMS keys can increase the risk of unauthorized access and data exposure, as managing access controls and auditing across multiple regions becomes more complex. This expanded attack surface may lead to compliance violations and data breaches.", + "RelatedUrl": "https://docs.aws.amazon.com/kms/latest/developerguide/multi-region-keys-overview.html#multi-region-concepts", + "Remediation": { + "Code": { + "CLI": "aws kms create-key --no-multi-region", + "NativeIaC": "", + "Other": "", + "Terraform": "resource \"aws_kms_key\" \"example\" { description = \"Single-region key\" multi_region = false }" + }, + "Recommendation": { + "Text": "Identify and replace multi-region keys with single-region KMS keys to enhance security and access control.", + "Url": "https://docs.aws.amazon.com/kms/latest/developerguide/mrk-when-to-use.html" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Multi-region keys should be used only when absolutely necessary, such as for cross-region disaster recovery, and should be carefully managed with strict access controls." +} diff --git a/prowler/providers/aws/services/kms/kms_cmk_not_multi_region/kms_cmk_not_multi_region.py b/prowler/providers/aws/services/kms/kms_cmk_not_multi_region/kms_cmk_not_multi_region.py new file mode 100644 index 0000000000..e96aa6a8e7 --- /dev/null +++ b/prowler/providers/aws/services/kms/kms_cmk_not_multi_region/kms_cmk_not_multi_region.py @@ -0,0 +1,25 @@ +from typing import List + +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.kms.kms_client import kms_client + + +class kms_cmk_not_multi_region(Check): + """kms_cmk_not_multi_region verifies if a KMS key is multi-regional""" + + def execute(self) -> List[Check_Report_AWS]: + findings = [] + + for key in kms_client.keys: + if key.manager == "CUSTOMER" and key.state == "Enabled": + report = Check_Report_AWS(metadata=self.metadata(), resource=key) + report.status = "PASS" + report.status_extended = f"KMS CMK {key.id} is a single-region key." + + if key.multi_region: + report.status = "FAIL" + report.status_extended = f"KMS CMK {key.id} is a multi-region key." + + findings.append(report) + + return findings diff --git a/prowler/providers/aws/services/kms/kms_service.py b/prowler/providers/aws/services/kms/kms_service.py index 4cd71fbc9d..e814516547 100644 --- a/prowler/providers/aws/services/kms/kms_service.py +++ b/prowler/providers/aws/services/kms/kms_service.py @@ -51,6 +51,7 @@ class KMS(AWSService): key.origin = response["KeyMetadata"]["Origin"] key.manager = response["KeyMetadata"]["KeyManager"] key.spec = response["KeyMetadata"]["CustomerMasterKeySpec"] + key.multi_region = response["KeyMetadata"]["MultiRegion"] except Exception as error: logger.error( f"{regional_client.region} -- {error.__class__.__name__}:{error.__traceback__.tb_lineno} -- {error}" @@ -121,4 +122,5 @@ class Key(BaseModel): policy: Optional[dict] spec: Optional[str] region: str + multi_region: Optional[bool] tags: Optional[list] = [] diff --git a/tests/providers/aws/services/kms/kms_cmk_not_multi_region/__init__.py b/tests/providers/aws/services/kms/kms_cmk_not_multi_region/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/providers/aws/services/kms/kms_cmk_not_multi_region/kms_cmk_not_multi_region_test.py b/tests/providers/aws/services/kms/kms_cmk_not_multi_region/kms_cmk_not_multi_region_test.py new file mode 100644 index 0000000000..a55618cdbf --- /dev/null +++ b/tests/providers/aws/services/kms/kms_cmk_not_multi_region/kms_cmk_not_multi_region_test.py @@ -0,0 +1,142 @@ +from unittest import mock + +from boto3 import client +from moto import mock_aws + +from tests.providers.aws.utils import AWS_REGION_US_EAST_1, set_mocked_aws_provider + + +class Test_kms_cmk_not_multi_region: + @mock_aws + def test_kms_no_keys(self) -> None: + from prowler.providers.aws.services.kms.kms_service import KMS + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.kms.kms_cmk_not_multi_region.kms_cmk_not_multi_region.kms_client", + new=KMS(aws_provider), + ), + ): + from prowler.providers.aws.services.kms.kms_cmk_not_multi_region.kms_cmk_not_multi_region import ( + kms_cmk_not_multi_region, + ) + + check = kms_cmk_not_multi_region() + result = check.execute() + + assert len(result) == 0 + + @mock_aws + def test_kms_cmk_disabled_key(self) -> None: + from prowler.providers.aws.services.kms.kms_service import KMS + + kms_client = client("kms", region_name=AWS_REGION_US_EAST_1) + key = kms_client.create_key()["KeyMetadata"] + kms_client.disable_key(KeyId=key["KeyId"]) + + aws_provider = set_mocked_aws_provider( + [AWS_REGION_US_EAST_1], scan_unused_services=False + ) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.kms.kms_cmk_not_multi_region.kms_cmk_not_multi_region.kms_client", + new=KMS(aws_provider), + ), + ): + from prowler.providers.aws.services.kms.kms_cmk_not_multi_region.kms_cmk_not_multi_region import ( + kms_cmk_not_multi_region, + ) + + check = kms_cmk_not_multi_region() + result = check.execute() + + assert len(result) == 0 + + @mock_aws + def test_kms_cmk_is_multi_regional(self) -> None: + kms_client = client("kms", region_name=AWS_REGION_US_EAST_1) + key = kms_client.create_key(MultiRegion=True)["KeyMetadata"] + + # The Prowler service import MUST be made within the decorated + # code not to make real API calls to the AWS service. + from prowler.providers.aws.services.kms.kms_service import KMS + + aws_provider = set_mocked_aws_provider( + [AWS_REGION_US_EAST_1], scan_unused_services=False + ) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.kms.kms_cmk_not_multi_region.kms_cmk_not_multi_region.kms_client", + new=KMS(aws_provider), + ), + ): + from prowler.providers.aws.services.kms.kms_cmk_not_multi_region.kms_cmk_not_multi_region import ( + kms_cmk_not_multi_region, + ) + + check = kms_cmk_not_multi_region() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"KMS CMK {key['KeyId']} is a multi-region key." + ) + assert result[0].resource_id == key["KeyId"] + assert result[0].resource_arn == key["Arn"] + + @mock_aws + def test_kms_cmk_is_single_regional(self) -> None: + kms_client = client("kms", region_name=AWS_REGION_US_EAST_1) + key = kms_client.create_key(MultiRegion=False)["KeyMetadata"] + + # The Prowler service import MUST be made within the decorated + # code not to make real API calls to the AWS service. + from prowler.providers.aws.services.kms.kms_service import KMS + + aws_provider = set_mocked_aws_provider( + [AWS_REGION_US_EAST_1], scan_unused_services=False + ) + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), + mock.patch( + "prowler.providers.aws.services.kms.kms_cmk_not_multi_region.kms_cmk_not_multi_region.kms_client", + new=KMS(aws_provider), + ), + ): + from prowler.providers.aws.services.kms.kms_cmk_not_multi_region.kms_cmk_not_multi_region import ( + kms_cmk_not_multi_region, + ) + + check = kms_cmk_not_multi_region() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"KMS CMK {key['KeyId']} is a single-region key." + ) + assert result[0].resource_id == key["KeyId"] + assert result[0].resource_arn == key["Arn"] diff --git a/tests/providers/aws/services/kms/kms_service_test.py b/tests/providers/aws/services/kms/kms_service_test.py index d08ca29564..082ccf129f 100644 --- a/tests/providers/aws/services/kms/kms_service_test.py +++ b/tests/providers/aws/services/kms/kms_service_test.py @@ -11,7 +11,7 @@ from tests.providers.aws.utils import ( ) -class Test_ACM_Service: +class Test_KMS_Service: # Test KMS Service @mock_aws @@ -68,6 +68,7 @@ class Test_ACM_Service: kms_client = client("kms", region_name=AWS_REGION_US_EAST_1) # Create KMS keys key1 = kms_client.create_key( + MultiRegion=False, Tags=[ {"TagKey": "test", "TagValue": "test"}, ], @@ -80,6 +81,7 @@ class Test_ACM_Service: assert kms.keys[0].state == key1["KeyState"] assert kms.keys[0].origin == key1["Origin"] assert kms.keys[0].manager == key1["KeyManager"] + assert kms.keys[0].multi_region == key1["MultiRegion"] assert kms.keys[0].tags == [ {"TagKey": "test", "TagValue": "test"}, ] @@ -90,8 +92,8 @@ class Test_ACM_Service: # Generate KMS Client kms_client = client("kms", region_name=AWS_REGION_US_EAST_1) # Create KMS keys - key1 = kms_client.create_key()["KeyMetadata"] - key2 = kms_client.create_key()["KeyMetadata"] + key1 = kms_client.create_key(MultiRegion=False)["KeyMetadata"] + key2 = kms_client.create_key(MultiRegion=False)["KeyMetadata"] kms_client.enable_key_rotation(KeyId=key2["KeyId"]) # KMS client for this test class aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) @@ -138,8 +140,12 @@ class Test_ACM_Service: # Generate KMS Client kms_client = client("kms", region_name=AWS_REGION_US_EAST_1) # Create KMS keys - key1 = kms_client.create_key(Policy=default_policy)["KeyMetadata"] - key2 = kms_client.create_key(Policy=public_policy)["KeyMetadata"] + key1 = kms_client.create_key(MultiRegion=False, Policy=default_policy)[ + "KeyMetadata" + ] + key2 = kms_client.create_key(MultiRegion=False, Policy=public_policy)[ + "KeyMetadata" + ] # KMS client for this test class aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) kms = KMS(aws_provider) From 914012de2b665807ae9d93415d202a54059e22dc Mon Sep 17 00:00:00 2001 From: Daniel Barranquero <74871504+danibarranqueroo@users.noreply.github.com> Date: Wed, 5 Feb 2025 18:39:49 +0100 Subject: [PATCH 11/30] fix(cloudfront): fix false positive in s3 origins (#6823) --- ..._distributions_origin_traffic_encrypted.py | 22 +++++--- ...ributions_origin_traffic_encrypted_test.py | 51 +++++++++++++++++++ 2 files changed, 65 insertions(+), 8 deletions(-) diff --git a/prowler/providers/aws/services/cloudfront/cloudfront_distributions_origin_traffic_encrypted/cloudfront_distributions_origin_traffic_encrypted.py b/prowler/providers/aws/services/cloudfront/cloudfront_distributions_origin_traffic_encrypted/cloudfront_distributions_origin_traffic_encrypted.py index 0046ecb570..a688bf3764 100644 --- a/prowler/providers/aws/services/cloudfront/cloudfront_distributions_origin_traffic_encrypted/cloudfront_distributions_origin_traffic_encrypted.py +++ b/prowler/providers/aws/services/cloudfront/cloudfront_distributions_origin_traffic_encrypted/cloudfront_distributions_origin_traffic_encrypted.py @@ -14,14 +14,20 @@ class cloudfront_distributions_origin_traffic_encrypted(Check): unencrypted_origins = [] for origin in distribution.origins: - if ( - origin.origin_protocol_policy == "" - or origin.origin_protocol_policy == "http-only" - ) or ( - origin.origin_protocol_policy == "match-viewer" - and distribution.viewer_protocol_policy == "allow-all" - ): - unencrypted_origins.append(origin.id) + if origin.s3_origin_config: + # For S3, only check the viewer protocol policy + if distribution.viewer_protocol_policy == "allow-all": + unencrypted_origins.append(origin.id) + else: + # Regular check for custom origins (ALB, EC2, API Gateway, etc.) + if ( + origin.origin_protocol_policy == "" + or origin.origin_protocol_policy == "http-only" + ) or ( + origin.origin_protocol_policy == "match-viewer" + and distribution.viewer_protocol_policy == "allow-all" + ): + unencrypted_origins.append(origin.id) if unencrypted_origins: report.status = "FAIL" diff --git a/tests/providers/aws/services/cloudfront/cloudfront_distributions_origin_traffic_encrypted/cloudfront_distributions_origin_traffic_encrypted_test.py b/tests/providers/aws/services/cloudfront/cloudfront_distributions_origin_traffic_encrypted/cloudfront_distributions_origin_traffic_encrypted_test.py index 4063794ee0..cb58ef4af1 100644 --- a/tests/providers/aws/services/cloudfront/cloudfront_distributions_origin_traffic_encrypted/cloudfront_distributions_origin_traffic_encrypted_test.py +++ b/tests/providers/aws/services/cloudfront/cloudfront_distributions_origin_traffic_encrypted/cloudfront_distributions_origin_traffic_encrypted_test.py @@ -225,3 +225,54 @@ class Test_cloudfront_distributions_origin_traffic_encrypted: == f"CloudFront Distribution {DISTRIBUTION_ID} does encrypt traffic to custom origins." ) assert result[0].resource_tags == [] + + def test_distribution_traffic_encrypted_with_s3_config(self): + cloudfront_client = mock.MagicMock + cloudfront_client.distributions = { + DISTRIBUTION_ID: Distribution( + arn=DISTRIBUTION_ARN, + id=DISTRIBUTION_ID, + region=REGION, + origins=[ + Origin( + id="origin1", + domain_name="asdf.s3.us-east-1.amazonaws.com", + origin_protocol_policy="", + origin_ssl_protocols=[], + s3_origin_config={ + "OriginAccessIdentity": "origin-access-identity/cloudfront/1234567890123456" + }, + ) + ], + default_cache_config=DefaultCacheConfigBehaviour( + realtime_log_config_arn="", + viewer_protocol_policy=ViewerProtocolPolicy.redirect_to_https, + field_level_encryption_id="", + ), + default_root_object="index.html", + viewer_protocol_policy="redirect-to-https", + ) + } + + with mock.patch( + "prowler.providers.aws.services.cloudfront.cloudfront_service.CloudFront", + new=cloudfront_client, + ): + # Test Check + from prowler.providers.aws.services.cloudfront.cloudfront_distributions_origin_traffic_encrypted.cloudfront_distributions_origin_traffic_encrypted import ( + cloudfront_distributions_origin_traffic_encrypted, + ) + + check = cloudfront_distributions_origin_traffic_encrypted() + result = check.execute() + + assert len(result) == 1 + assert result[0].region == REGION + assert result[0].resource_arn == DISTRIBUTION_ARN + assert result[0].resource_id == DISTRIBUTION_ID + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"CloudFront Distribution {DISTRIBUTION_ID} does encrypt traffic to custom origins." + ) + assert result[0].resource_tags == [] From bce958b8e6ade6e605066ba0a342bc8adf284ab0 Mon Sep 17 00:00:00 2001 From: Mario Rodriguez Lopez <101330800+MarioRgzLpz@users.noreply.github.com> Date: Wed, 5 Feb 2025 18:45:48 +0100 Subject: [PATCH 12/30] feat(entra): add new check `entra_thirdparty_integrated_apps_not_allowed` (#6357) Co-authored-by: MrCloudSec --- prowler/lib/check/models.py | 2 - ...ttings_password_never_expire.metadata.json | 2 +- ...incenter_settings_password_never_expire.py | 1 - .../services/entra/entra_service.py | 90 ++++++------- .../__init__.py | 0 ..._integrated_apps_not_allowed.metadata.json | 30 +++++ ..._thirdparty_integrated_apps_not_allowed.py | 26 ++++ ...dparty_integrated_apps_not_allowed_test.py | 127 ++++++++++++++++++ 8 files changed, 227 insertions(+), 51 deletions(-) create mode 100644 prowler/providers/microsoft365/services/entra/entra_thirdparty_integrated_apps_not_allowed/__init__.py create mode 100644 prowler/providers/microsoft365/services/entra/entra_thirdparty_integrated_apps_not_allowed/entra_thirdparty_integrated_apps_not_allowed.metadata.json create mode 100644 prowler/providers/microsoft365/services/entra/entra_thirdparty_integrated_apps_not_allowed/entra_thirdparty_integrated_apps_not_allowed.py create mode 100644 tests/providers/microsoft365/services/entra/entra_thirdparty_integrated_apps_not_allowed/entra_thirdparty_integrated_apps_not_allowed_test.py diff --git a/prowler/lib/check/models.py b/prowler/lib/check/models.py index 928d68f075..2aca0e83c1 100644 --- a/prowler/lib/check/models.py +++ b/prowler/lib/check/models.py @@ -541,7 +541,6 @@ class Check_Report_Microsoft365(Check_Report): resource_name: str resource_id: str - tenant_id: str location: str def __init__(self, metadata: Dict, resource: Any) -> None: @@ -556,7 +555,6 @@ class Check_Report_Microsoft365(Check_Report): resource, "name", getattr(resource, "resource_name", "") ) self.resource_id = getattr(resource, "id", getattr(resource, "resource_id", "")) - self.tenant_id = getattr(resource, "tenant_id", "") self.location = getattr(resource, "location", "global") diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_settings_password_never_expire/admincenter_settings_password_never_expire.metadata.json b/prowler/providers/microsoft365/services/admincenter/admincenter_settings_password_never_expire/admincenter_settings_password_never_expire.metadata.json index 13528b0008..a1b009df6a 100644 --- a/prowler/providers/microsoft365/services/admincenter/admincenter_settings_password_never_expire/admincenter_settings_password_never_expire.metadata.json +++ b/prowler/providers/microsoft365/services/admincenter/admincenter_settings_password_never_expire/admincenter_settings_password_never_expire.metadata.json @@ -1,5 +1,5 @@ { - "Provider": "microsoft", + "Provider": "microsoft365", "CheckID": "admincenter_settings_password_never_expire", "CheckTitle": "Ensure the 'Password expiration policy' is set to 'Set passwords to never expire (recommended)'", "CheckType": [], diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_settings_password_never_expire/admincenter_settings_password_never_expire.py b/prowler/providers/microsoft365/services/admincenter/admincenter_settings_password_never_expire/admincenter_settings_password_never_expire.py index 8de62dd8d5..0bbdd3c8a1 100644 --- a/prowler/providers/microsoft365/services/admincenter/admincenter_settings_password_never_expire/admincenter_settings_password_never_expire.py +++ b/prowler/providers/microsoft365/services/admincenter/admincenter_settings_password_never_expire/admincenter_settings_password_never_expire.py @@ -9,7 +9,6 @@ class admincenter_settings_password_never_expire(Check): findings = [] for domain in admincenter_client.domains.values(): report = Check_Report_Microsoft365(self.metadata(), resource=domain) - report.resource_id = domain.id report.resource_name = domain.id report.status = "FAIL" report.status_extended = ( diff --git a/prowler/providers/microsoft365/services/entra/entra_service.py b/prowler/providers/microsoft365/services/entra/entra_service.py index 8561a31edd..14d04ad93a 100644 --- a/prowler/providers/microsoft365/services/entra/entra_service.py +++ b/prowler/providers/microsoft365/services/entra/entra_service.py @@ -25,7 +25,7 @@ class Entra(Microsoft365Service): async def _get_authorization_policy(self): logger.info("Entra - Getting authorization policy...") - authorization_policy = {} + authorization_policy = None try: auth_policy = await self.client.policies.authorization_policy.get() @@ -33,52 +33,48 @@ class Entra(Microsoft365Service): auth_policy, "default_user_role_permissions", None ) - authorization_policy.update( - { - auth_policy.id: AuthorizationPolicy( - id=auth_policy.id, - name=auth_policy.display_name, - description=auth_policy.description, - default_user_role_permissions=DefaultUserRolePermissions( - allowed_to_create_apps=getattr( - default_user_role_permissions, - "allowed_to_create_apps", - None, - ), - allowed_to_create_security_groups=getattr( - default_user_role_permissions, - "allowed_to_create_security_groups", - None, - ), - allowed_to_create_tenants=getattr( - default_user_role_permissions, - "allowed_to_create_tenants", - None, - ), - allowed_to_read_bitlocker_keys_for_owned_device=getattr( - default_user_role_permissions, - "allowed_to_read_bitlocker_keys_for_owned_device", - None, - ), - allowed_to_read_other_users=getattr( - default_user_role_permissions, - "allowed_to_read_other_users", - None, - ), - odata_type=getattr( - default_user_role_permissions, "odata_type", None - ), - permission_grant_policies_assigned=[ - policy_assigned - for policy_assigned in getattr( - default_user_role_permissions, - "permission_grant_policies_assigned", - [], - ) - ], - ), - ) - } + authorization_policy = AuthorizationPolicy( + id=auth_policy.id, + name=auth_policy.display_name, + description=auth_policy.description, + default_user_role_permissions=DefaultUserRolePermissions( + allowed_to_create_apps=getattr( + default_user_role_permissions, + "allowed_to_create_apps", + None, + ), + allowed_to_create_security_groups=getattr( + default_user_role_permissions, + "allowed_to_create_security_groups", + None, + ), + allowed_to_create_tenants=getattr( + default_user_role_permissions, + "allowed_to_create_tenants", + None, + ), + allowed_to_read_bitlocker_keys_for_owned_device=getattr( + default_user_role_permissions, + "allowed_to_read_bitlocker_keys_for_owned_device", + None, + ), + allowed_to_read_other_users=getattr( + default_user_role_permissions, + "allowed_to_read_other_users", + None, + ), + odata_type=getattr( + default_user_role_permissions, "odata_type", None + ), + permission_grant_policies_assigned=[ + policy_assigned + for policy_assigned in getattr( + default_user_role_permissions, + "permission_grant_policies_assigned", + [], + ) + ], + ), ) except Exception as error: logger.error( diff --git a/prowler/providers/microsoft365/services/entra/entra_thirdparty_integrated_apps_not_allowed/__init__.py b/prowler/providers/microsoft365/services/entra/entra_thirdparty_integrated_apps_not_allowed/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/microsoft365/services/entra/entra_thirdparty_integrated_apps_not_allowed/entra_thirdparty_integrated_apps_not_allowed.metadata.json b/prowler/providers/microsoft365/services/entra/entra_thirdparty_integrated_apps_not_allowed/entra_thirdparty_integrated_apps_not_allowed.metadata.json new file mode 100644 index 0000000000..85e831f120 --- /dev/null +++ b/prowler/providers/microsoft365/services/entra/entra_thirdparty_integrated_apps_not_allowed/entra_thirdparty_integrated_apps_not_allowed.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "microsoft365", + "CheckID": "entra_thirdparty_integrated_apps_not_allowed", + "CheckTitle": "Ensure third party integrated applications are not allowed", + "CheckType": [], + "ServiceName": "entra", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "", + "Description": "Require administrators or appropriately delegated users to register third-party applications.", + "Risk": "It is recommended to only allow an administrator to register custom-developed applications. This ensures that the application undergoes a formal security review and approval process prior to exposing Azure Active Directory data. Certain users like developers or other high-request users may also be delegated permissions to prevent them from waiting on an administrative user. Your organization should review your policies and decide your needs.", + "RelatedUrl": "https://learn.microsoft.com/en-us/entra/identity-platform/how-applications-are-added#who-has-permission-to-add-applications-to-my-microsoft-entra-instance", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "1. From Entra select the Portal Menu 2. Select Azure Active Directory 3. Select Users 4. Select User settings 5. Ensure that Users can register applications is set to No", + "Url": "https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/delegate-app-roles#restrict-who-can-create-applications" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Enforcing this setting will create additional requests for approval that will need to be addressed by an administrator. If permissions are delegated, a user may approve a malevolent third party application, potentially giving it access to your data." +} diff --git a/prowler/providers/microsoft365/services/entra/entra_thirdparty_integrated_apps_not_allowed/entra_thirdparty_integrated_apps_not_allowed.py b/prowler/providers/microsoft365/services/entra/entra_thirdparty_integrated_apps_not_allowed/entra_thirdparty_integrated_apps_not_allowed.py new file mode 100644 index 0000000000..f31b8528a5 --- /dev/null +++ b/prowler/providers/microsoft365/services/entra/entra_thirdparty_integrated_apps_not_allowed/entra_thirdparty_integrated_apps_not_allowed.py @@ -0,0 +1,26 @@ +from prowler.lib.check.models import Check, Check_Report_Microsoft365 +from prowler.providers.microsoft365.services.entra.entra_client import entra_client + + +class entra_thirdparty_integrated_apps_not_allowed(Check): + def execute(self) -> Check_Report_Microsoft365: + findings = [] + + auth_policy = entra_client.authorization_policy + report = Check_Report_Microsoft365(self.metadata(), auth_policy) + report.resource_name = getattr(auth_policy, "name", "Authorization Policy") + report.resource_id = getattr(auth_policy, "id", "authorizationPolicy") + report.status = "FAIL" + report.status_extended = "App creation is not disabled for non-admin users." + + if getattr(auth_policy, "default_user_role_permissions", None) and not getattr( + auth_policy.default_user_role_permissions, + "allowed_to_create_apps", + True, + ): + report.status = "PASS" + report.status_extended = "App creation is disabled for non-admin users." + + findings.append(report) + + return findings diff --git a/tests/providers/microsoft365/services/entra/entra_thirdparty_integrated_apps_not_allowed/entra_thirdparty_integrated_apps_not_allowed_test.py b/tests/providers/microsoft365/services/entra/entra_thirdparty_integrated_apps_not_allowed/entra_thirdparty_integrated_apps_not_allowed_test.py new file mode 100644 index 0000000000..79884c0531 --- /dev/null +++ b/tests/providers/microsoft365/services/entra/entra_thirdparty_integrated_apps_not_allowed/entra_thirdparty_integrated_apps_not_allowed_test.py @@ -0,0 +1,127 @@ +from unittest import mock +from uuid import uuid4 + +from prowler.providers.microsoft365.services.entra.entra_service import ( + DefaultUserRolePermissions, +) +from tests.providers.microsoft365.microsoft365_fixtures import ( + DOMAIN, + set_mocked_microsoft365_provider, +) + + +class Test_entra_thirdparty_integrated_apps_not_allowed: + def test_entra_no_authorization_policy(self): + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_microsoft365_provider(), + ), + mock.patch( + "prowler.providers.microsoft365.services.entra.entra_thirdparty_integrated_apps_not_allowed.entra_thirdparty_integrated_apps_not_allowed.entra_client", + new=entra_client, + ), + ): + from prowler.providers.microsoft365.services.entra.entra_thirdparty_integrated_apps_not_allowed.entra_thirdparty_integrated_apps_not_allowed import ( + entra_thirdparty_integrated_apps_not_allowed, + ) + + entra_client.authorization_policy = None + + check = entra_thirdparty_integrated_apps_not_allowed() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].resource_name == "Authorization Policy" + assert result[0].resource_id == "authorizationPolicy" + assert ( + result[0].status_extended + == "App creation is not disabled for non-admin users." + ) + + def test_entra_default_user_role_permissions_not_allowed_to_create_apps(self): + id = str(uuid4()) + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_microsoft365_provider(), + ), + mock.patch( + "prowler.providers.microsoft365.services.entra.entra_thirdparty_integrated_apps_not_allowed.entra_thirdparty_integrated_apps_not_allowed.entra_client", + new=entra_client, + ), + ): + from prowler.providers.microsoft365.services.entra.entra_service import ( + AuthorizationPolicy, + ) + from prowler.providers.microsoft365.services.entra.entra_thirdparty_integrated_apps_not_allowed.entra_thirdparty_integrated_apps_not_allowed import ( + entra_thirdparty_integrated_apps_not_allowed, + ) + + role_permissions = DefaultUserRolePermissions(allowed_to_create_apps=False) + entra_client.authorization_policy = AuthorizationPolicy( + id=id, + name="Test", + description="Test", + default_user_role_permissions=role_permissions, + ) + + check = entra_thirdparty_integrated_apps_not_allowed() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "App creation is disabled for non-admin users." + ) + assert result[0].resource_name == "Test" + assert result[0].resource_id == id + + def test_entra_default_user_role_permissions_allowed_to_create_apps(self): + id = str(uuid4()) + entra_client = mock.MagicMock + entra_client.audited_tenant = "audited_tenant" + entra_client.audited_domain = DOMAIN + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_microsoft365_provider(), + ), + mock.patch( + "prowler.providers.microsoft365.services.entra.entra_thirdparty_integrated_apps_not_allowed.entra_thirdparty_integrated_apps_not_allowed.entra_client", + new=entra_client, + ), + ): + from prowler.providers.microsoft365.services.entra.entra_service import ( + AuthorizationPolicy, + ) + from prowler.providers.microsoft365.services.entra.entra_thirdparty_integrated_apps_not_allowed.entra_thirdparty_integrated_apps_not_allowed import ( + entra_thirdparty_integrated_apps_not_allowed, + ) + + role_permissions = DefaultUserRolePermissions(allowed_to_create_apps=True) + entra_client.authorization_policy = AuthorizationPolicy( + id=id, + name="Test", + description="Test", + default_user_role_permissions=role_permissions, + ) + + check = entra_thirdparty_integrated_apps_not_allowed() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "App creation is not disabled for non-admin users." + ) + assert result[0].resource_name == "Test" + assert result[0].resource_id == id From 9ba657c2618ee707bbff1793ab10cc18a81bf808 Mon Sep 17 00:00:00 2001 From: Sergio Garcia Date: Wed, 5 Feb 2025 14:03:31 -0500 Subject: [PATCH 13/30] fix(kms): handle error in DescribeKey function (#6839) --- .../providers/aws/services/kms/kms_service.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/prowler/providers/aws/services/kms/kms_service.py b/prowler/providers/aws/services/kms/kms_service.py index e814516547..a4ef49ec57 100644 --- a/prowler/providers/aws/services/kms/kms_service.py +++ b/prowler/providers/aws/services/kms/kms_service.py @@ -45,13 +45,18 @@ class KMS(AWSService): logger.info("KMS - Describing Key...") try: for key in self.keys: - regional_client = self.regional_clients[key.region] - response = regional_client.describe_key(KeyId=key.id) - key.state = response["KeyMetadata"]["KeyState"] - key.origin = response["KeyMetadata"]["Origin"] - key.manager = response["KeyMetadata"]["KeyManager"] - key.spec = response["KeyMetadata"]["CustomerMasterKeySpec"] - key.multi_region = response["KeyMetadata"]["MultiRegion"] + try: + regional_client = self.regional_clients[key.region] + response = regional_client.describe_key(KeyId=key.id) + key.state = response["KeyMetadata"]["KeyState"] + key.origin = response["KeyMetadata"]["Origin"] + key.manager = response["KeyMetadata"]["KeyManager"] + key.spec = response["KeyMetadata"]["CustomerMasterKeySpec"] + key.multi_region = response["KeyMetadata"]["MultiRegion"] + except Exception as error: + logger.error( + f"{regional_client.region} -- {error.__class__.__name__}:{error.__traceback__.tb_lineno} -- {error}" + ) except Exception as error: logger.error( f"{regional_client.region} -- {error.__class__.__name__}:{error.__traceback__.tb_lineno} -- {error}" From b89b1a64f4487093c3f355dc3cca21cc52307e65 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 5 Feb 2025 18:02:42 -0500 Subject: [PATCH 14/30] chore(deps): bump trufflesecurity/trufflehog from 3.88.4 to 3.88.5 (#6844) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/find-secrets.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/find-secrets.yml b/.github/workflows/find-secrets.yml index 0eb7bbd6fe..3f2c135ddf 100644 --- a/.github/workflows/find-secrets.yml +++ b/.github/workflows/find-secrets.yml @@ -11,7 +11,7 @@ jobs: with: fetch-depth: 0 - name: TruffleHog OSS - uses: trufflesecurity/trufflehog@v3.88.4 + uses: trufflesecurity/trufflehog@v3.88.5 with: path: ./ base: ${{ github.event.repository.default_branch }} From 665bfa2f13229c4ad65ed759fd247d8db7b5f7f8 Mon Sep 17 00:00:00 2001 From: Prowler Bot Date: Thu, 6 Feb 2025 14:46:32 +0100 Subject: [PATCH 15/30] chore(regions_update): Changes in regions for AWS services (#6848) Co-authored-by: MrCloudSec <38561120+MrCloudSec@users.noreply.github.com> --- prowler/providers/aws/aws_regions_by_service.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/prowler/providers/aws/aws_regions_by_service.json b/prowler/providers/aws/aws_regions_by_service.json index d9ed6b445c..49e1c23665 100644 --- a/prowler/providers/aws/aws_regions_by_service.json +++ b/prowler/providers/aws/aws_regions_by_service.json @@ -145,6 +145,7 @@ "ap-southeast-3", "ap-southeast-4", "ap-southeast-5", + "ap-southeast-7", "ca-central-1", "ca-west-1", "eu-central-1", @@ -158,6 +159,7 @@ "il-central-1", "me-central-1", "me-south-1", + "mx-central-1", "sa-east-1", "us-east-1", "us-east-2", @@ -9359,6 +9361,7 @@ "ap-southeast-2", "ap-southeast-3", "ap-southeast-4", + "ap-southeast-5", "ca-central-1", "ca-west-1", "eu-central-1", From 88cfc0fa7e166be4688f617ee97d23cda691e7ea Mon Sep 17 00:00:00 2001 From: Pranay Girase Date: Thu, 6 Feb 2025 21:12:31 +0530 Subject: [PATCH 16/30] fix(typo): typos in Dashboard and Report in HTML (#6847) --- dashboard/pages/overview.py | 4 ++-- prowler/lib/outputs/html/html.py | 2 +- tests/lib/outputs/html/html_test.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dashboard/pages/overview.py b/dashboard/pages/overview.py index 24ffae5de6..afa7ecba04 100644 --- a/dashboard/pages/overview.py +++ b/dashboard/pages/overview.py @@ -1720,7 +1720,7 @@ def generate_table(data, index, color_mapping_severity, color_mapping_status): [ html.P( html.Strong( - "Recomendation: ", + "Recommendation: ", style={ "margin-right": "5px" }, @@ -1744,7 +1744,7 @@ def generate_table(data, index, color_mapping_severity, color_mapping_status): [ html.P( html.Strong( - "RecomendationUrl: ", + "RecommendationUrl: ", style={ "margin-right": "5px" }, diff --git a/prowler/lib/outputs/html/html.py b/prowler/lib/outputs/html/html.py index f1944b6ee6..f981996fdc 100644 --- a/prowler/lib/outputs/html/html.py +++ b/prowler/lib/outputs/html/html.py @@ -205,7 +205,7 @@ class HTML(Output): Resource Tags Status Extended Risk - Recomendation + Recommendation Compliance diff --git a/tests/lib/outputs/html/html_test.py b/tests/lib/outputs/html/html_test.py index 0fa5ccbe3e..f38b1c65ab 100644 --- a/tests/lib/outputs/html/html_test.py +++ b/tests/lib/outputs/html/html_test.py @@ -339,7 +339,7 @@ def get_aws_html_header(args: list) -> str: Resource Tags Status Extended Risk - Recomendation + Recommendation Compliance From 9b5f676f504b354c57cdcc024189532871449379 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Fern=C3=A1ndez=20Poyatos?= Date: Fri, 7 Feb 2025 09:09:55 +0100 Subject: [PATCH 17/30] feat(findings): Require date filters for findings endpoints (#6800) --- api/src/backend/api/CHANGELOG.md | 14 ++++++ api/src/backend/api/filters.py | 58 +++++++++++++++++++++-- api/src/backend/api/specs/v1.yaml | 12 +++++ api/src/backend/api/tests/test_views.py | 63 +++++++++++++++++++------ api/src/backend/api/v1/views.py | 22 +++++++++ api/src/backend/config/django/base.py | 1 + 6 files changed, 152 insertions(+), 18 deletions(-) create mode 100644 api/src/backend/api/CHANGELOG.md diff --git a/api/src/backend/api/CHANGELOG.md b/api/src/backend/api/CHANGELOG.md new file mode 100644 index 0000000000..6f8971f40d --- /dev/null +++ b/api/src/backend/api/CHANGELOG.md @@ -0,0 +1,14 @@ +# Prowler API Changelog + +All notable changes to the **Prowler API** are documented in this file. + + +--- + +## [Unreleased] + +### Changed +- Daily scheduled scan instances are now created beforehand with `SCHEDULED` state [(#6700)](https://github.com/prowler-cloud/prowler/pull/6700). +- Findings endpoints now require at least one date filter [(#6800)](https://github.com/prowler-cloud/prowler/pull/6800). + +--- diff --git a/api/src/backend/api/filters.py b/api/src/backend/api/filters.py index 0ace11fe9c..ed4ef1c152 100644 --- a/api/src/backend/api/filters.py +++ b/api/src/backend/api/filters.py @@ -1,4 +1,4 @@ -from datetime import date, datetime, timezone +from datetime import date, datetime, timedelta, timezone from django.conf import settings from django.db.models import Q @@ -346,8 +346,14 @@ class FindingFilter(FilterSet): inserted_at = DateFilter(method="filter_inserted_at", lookup_expr="date") inserted_at__date = DateFilter(method="filter_inserted_at", lookup_expr="date") - inserted_at__gte = DateFilter(method="filter_inserted_at_gte") - inserted_at__lte = DateFilter(method="filter_inserted_at_lte") + inserted_at__gte = DateFilter( + method="filter_inserted_at_gte", + help_text=f"Maximum date range is {settings.FINDINGS_MAX_DAYS_IN_RANGE} days.", + ) + inserted_at__lte = DateFilter( + method="filter_inserted_at_lte", + help_text=f"Maximum date range is {settings.FINDINGS_MAX_DAYS_IN_RANGE} days.", + ) class Meta: model = Finding @@ -375,6 +381,52 @@ class FindingFilter(FilterSet): }, } + def filter_queryset(self, queryset): + if not (self.data.get("scan") or self.data.get("scan__in")) and not ( + self.data.get("inserted_at") + or self.data.get("inserted_at__date") + or self.data.get("inserted_at__gte") + or self.data.get("inserted_at__lte") + ): + raise ValidationError( + [ + { + "detail": "At least one date filter is required: filter[inserted_at], filter[inserted_at.gte], " + "or filter[inserted_at.lte].", + "status": 400, + "source": {"pointer": "/data/attributes/inserted_at"}, + "code": "required", + } + ] + ) + + gte_date = ( + datetime.strptime(self.data.get("inserted_at__gte"), "%Y-%m-%d").date() + if self.data.get("inserted_at__gte") + else datetime.now(timezone.utc).date() + ) + lte_date = ( + datetime.strptime(self.data.get("inserted_at__lte"), "%Y-%m-%d").date() + if self.data.get("inserted_at__lte") + else datetime.now(timezone.utc).date() + ) + + if abs(lte_date - gte_date) > timedelta( + days=settings.FINDINGS_MAX_DAYS_IN_RANGE + ): + raise ValidationError( + [ + { + "detail": f"The date range cannot exceed {settings.FINDINGS_MAX_DAYS_IN_RANGE} days.", + "status": 400, + "source": {"pointer": "/data/attributes/inserted_at"}, + "code": "invalid", + } + ] + ) + + return super().filter_queryset(queryset) + # Convert filter values to UUIDv7 values for use with partitioning def filter_scan_id(self, queryset, name, value): try: diff --git a/api/src/backend/api/specs/v1.yaml b/api/src/backend/api/specs/v1.yaml index ab25ef0e27..6b791f885b 100644 --- a/api/src/backend/api/specs/v1.yaml +++ b/api/src/backend/api/specs/v1.yaml @@ -346,6 +346,9 @@ paths: schema: type: string format: date + description: At least one of the variations of the `filter[inserted_at]` filter + must be provided. + required: true - in: query name: filter[inserted_at__date] schema: @@ -356,11 +359,13 @@ paths: schema: type: string format: date + description: Maximum date range is 7 days. - in: query name: filter[inserted_at__lte] schema: type: string format: date + description: Maximum date range is 7 days. - in: query name: filter[provider] schema: @@ -861,11 +866,13 @@ paths: schema: type: string format: date + description: Maximum date range is 7 days. - in: query name: filter[inserted_at__lte] schema: type: string format: date + description: Maximum date range is 7 days. - in: query name: filter[provider] schema: @@ -1275,6 +1282,9 @@ paths: schema: type: string format: date + description: At least one of the variations of the `filter[inserted_at]` filter + must be provided. + required: true - in: query name: filter[inserted_at__date] schema: @@ -1285,11 +1295,13 @@ paths: schema: type: string format: date + description: Maximum date range is 7 days. - in: query name: filter[inserted_at__lte] schema: type: string format: date + description: Maximum date range is 7 days. - in: query name: filter[provider] schema: diff --git a/api/src/backend/api/tests/test_views.py b/api/src/backend/api/tests/test_views.py index 0173797062..d0c0b91717 100644 --- a/api/src/backend/api/tests/test_views.py +++ b/api/src/backend/api/tests/test_views.py @@ -5,6 +5,7 @@ from unittest.mock import ANY, Mock, patch import jwt import pytest from conftest import API_JSON_CONTENT_TYPE, TEST_PASSWORD, TEST_USER +from django.conf import settings from django.urls import reverse from rest_framework import status @@ -27,6 +28,12 @@ from api.rls import Tenant TODAY = str(datetime.today().date()) +def today_after_n_days(n_days: int) -> str: + return datetime.strftime( + datetime.today().date() + timedelta(days=n_days), "%Y-%m-%d" + ) + + @pytest.mark.django_db class TestUserViewSet: def test_users_list(self, authenticated_client, create_test_user): @@ -2379,12 +2386,33 @@ class TestResourceViewSet: @pytest.mark.django_db class TestFindingViewSet: def test_findings_list_none(self, authenticated_client): - response = authenticated_client.get(reverse("finding-list")) + response = authenticated_client.get( + reverse("finding-list"), {"filter[inserted_at]": TODAY} + ) assert response.status_code == status.HTTP_200_OK assert len(response.json()["data"]) == 0 - def test_findings_list(self, authenticated_client, findings_fixture): + def test_findings_list_no_date_filter(self, authenticated_client): response = authenticated_client.get(reverse("finding-list")) + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.json()["errors"][0]["code"] == "required" + + def test_findings_date_range_too_large(self, authenticated_client): + response = authenticated_client.get( + reverse("finding-list"), + { + "filter[inserted_at.lte]": today_after_n_days( + -(settings.FINDINGS_MAX_DAYS_IN_RANGE + 1) + ), + }, + ) + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.json()["errors"][0]["code"] == "invalid" + + def test_findings_list(self, authenticated_client, findings_fixture): + response = authenticated_client.get( + reverse("finding-list"), {"filter[inserted_at]": TODAY} + ) assert response.status_code == status.HTTP_200_OK assert len(response.json()["data"]) == len(findings_fixture) assert ( @@ -2404,7 +2432,8 @@ class TestFindingViewSet: self, include_values, expected_resources, authenticated_client, findings_fixture ): response = authenticated_client.get( - reverse("finding-list"), {"include": include_values} + reverse("finding-list"), + {"include": include_values, "filter[inserted_at]": TODAY}, ) assert response.status_code == status.HTTP_200_OK assert len(response.json()["data"]) == len(findings_fixture) @@ -2439,13 +2468,13 @@ class TestFindingViewSet: ("service.icontains", "ec", 1), ("inserted_at", "2024-01-01", 0), ("inserted_at.date", "2024-01-01", 0), - ("inserted_at.gte", "2024-01-01", 2), + ("inserted_at.gte", today_after_n_days(-1), 2), ( "inserted_at.lte", - "2028-12-31", + today_after_n_days(1), 2, - ), # TODO: To avoid having to modify this value and to ensure that the tests always work, we should set the time before the fixtures are inserted - ("updated_at.lte", "2024-01-01", 0), + ), + ("updated_at.lte", today_after_n_days(-1), 0), ("resource_type.icontains", "prowler", 2), # full text search on finding ("search", "dev-qa", 1), @@ -2475,9 +2504,13 @@ class TestFindingViewSet: filter_value, expected_count, ): + filters = {f"filter[{filter_name}]": filter_value} + if "inserted_at" not in filter_name: + filters["filter[inserted_at]"] = TODAY + response = authenticated_client.get( reverse("finding-list"), - {f"filter[{filter_name}]": filter_value}, + filters, ) assert response.status_code == status.HTTP_200_OK @@ -2486,9 +2519,7 @@ class TestFindingViewSet: def test_finding_filter_by_scan_id(self, authenticated_client, findings_fixture): response = authenticated_client.get( reverse("finding-list"), - { - "filter[scan]": findings_fixture[0].scan.id, - }, + {"filter[scan]": findings_fixture[0].scan.id}, ) assert response.status_code == status.HTTP_200_OK assert len(response.json()["data"]) == 2 @@ -2511,6 +2542,7 @@ class TestFindingViewSet: reverse("finding-list"), { "filter[provider]": findings_fixture[0].scan.provider.id, + "filter[inserted_at]": TODAY, }, ) assert response.status_code == status.HTTP_200_OK @@ -2525,7 +2557,8 @@ class TestFindingViewSet: "filter[provider.in]": [ findings_fixture[0].scan.provider.id, findings_fixture[1].scan.provider.id, - ] + ], + "filter[inserted_at]": TODAY, }, ) assert response.status_code == status.HTTP_200_OK @@ -2559,13 +2592,13 @@ class TestFindingViewSet: ) def test_findings_sort(self, authenticated_client, sort_field): response = authenticated_client.get( - reverse("finding-list"), {"sort": sort_field} + reverse("finding-list"), {"sort": sort_field, "filter[inserted_at]": TODAY} ) assert response.status_code == status.HTTP_200_OK def test_findings_sort_invalid(self, authenticated_client): response = authenticated_client.get( - reverse("finding-list"), {"sort": "invalid"} + reverse("finding-list"), {"sort": "invalid", "filter[inserted_at]": TODAY} ) assert response.status_code == status.HTTP_400_BAD_REQUEST assert response.json()["errors"][0]["code"] == "invalid" @@ -2633,7 +2666,7 @@ class TestFindingViewSet: reverse("finding-metadata"), { "filter[severity__in]": ["low", "medium"], - "filter[inserted_at]": finding_1.updated_at.strftime("%Y-%m-%d"), + "filter[inserted_at]": finding_1.inserted_at.strftime("%Y-%m-%d"), }, ) data = response.json() diff --git a/api/src/backend/api/v1/views.py b/api/src/backend/api/v1/views.py index b11626c829..13c1d43f23 100644 --- a/api/src/backend/api/v1/views.py +++ b/api/src/backend/api/v1/views.py @@ -1271,6 +1271,14 @@ class ResourceViewSet(BaseRLSViewSet): tags=["Finding"], summary="List all findings", description="Retrieve a list of all findings with options for filtering by various criteria.", + parameters=[ + OpenApiParameter( + name="filter[inserted_at]", + description="At least one of the variations of the `filter[inserted_at]` filter must be provided.", + required=True, + type=OpenApiTypes.DATE, + ) + ], ), retrieve=extend_schema( tags=["Finding"], @@ -1288,6 +1296,14 @@ class ResourceViewSet(BaseRLSViewSet): tags=["Finding"], summary="Retrieve metadata values from findings", description="Fetch unique metadata values from a set of findings. This is useful for dynamic filtering.", + parameters=[ + OpenApiParameter( + name="filter[inserted_at]", + description="At least one of the variations of the `filter[inserted_at]` filter must be provided.", + required=True, + type=OpenApiTypes.DATE, + ) + ], filters=True, ), ) @@ -1364,6 +1380,12 @@ class FindingViewSet(BaseRLSViewSet): return queryset + def filter_queryset(self, queryset): + # Do not apply filters when retrieving specific finding + if self.action == "retrieve": + return queryset + return super().filter_queryset(queryset) + def inserted_at_to_uuidv7(self, inserted_at): if inserted_at is None: return None diff --git a/api/src/backend/config/django/base.py b/api/src/backend/config/django/base.py index f78cc94ab8..6494bd0ff3 100644 --- a/api/src/backend/config/django/base.py +++ b/api/src/backend/config/django/base.py @@ -207,3 +207,4 @@ CACHE_STALE_WHILE_REVALIDATE = env.int("DJANGO_STALE_WHILE_REVALIDATE", 60) TESTING = False +FINDINGS_MAX_DAYS_IN_RANGE = env.int("DJANGO_FINDINGS_MAX_DAYS_IN_RANGE", 7) From fb5ea3c3246799433e88f5fa5637f918e6cb7e20 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 7 Feb 2025 11:07:43 +0100 Subject: [PATCH 18/30] chore(deps): bump microsoft-kiota-abstractions from 1.9.1 to 1.9.2 (#6856) 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 0f53a98c43..a2e8bd9b29 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2359,13 +2359,13 @@ files = [ [[package]] name = "microsoft-kiota-abstractions" -version = "1.9.1" +version = "1.9.2" description = "Core abstractions for kiota generated libraries in Python" optional = false python-versions = "<4.0,>=3.9" files = [ - {file = "microsoft_kiota_abstractions-1.9.1-py3-none-any.whl", hash = "sha256:aca6147946fc56167038faddda0a07026b0ec4e76666520586de7ac5c1233295"}, - {file = "microsoft_kiota_abstractions-1.9.1.tar.gz", hash = "sha256:291f4f88bec5452b2c6bafed4927640d2c7d2f41b08c941ee786f6ecad3dcaa5"}, + {file = "microsoft_kiota_abstractions-1.9.2-py3-none-any.whl", hash = "sha256:a8853d272a84da59d6a2fe11a76c28e9c55bdab268a345ba48e918cb6822b607"}, + {file = "microsoft_kiota_abstractions-1.9.2.tar.gz", hash = "sha256:29cdafe8d0672f23099556e0b120dca6231c752cca9393e1e0092fa9ca594572"}, ] [package.dependencies] @@ -5090,4 +5090,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.13" -content-hash = "7511c2a5abad01f351b1d222d73a6b2c5374f386e8c035cce79934daca7ea762" +content-hash = "04e2981c05b80afbe9c4794211db4533333007ad51cd875aca8efbbbc9b8a958" diff --git a/pyproject.toml b/pyproject.toml index b9cb7f8b1f..083cdefb95 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,7 +59,7 @@ google-api-python-client = "2.160.0" google-auth-httplib2 = ">=0.1,<0.3" jsonschema = "4.23.0" kubernetes = "32.0.0" -microsoft-kiota-abstractions = "1.9.1" +microsoft-kiota-abstractions = "1.9.2" msgraph-sdk = "1.18.0" numpy = "2.0.2" pandas = "2.2.3" From 5582265e9dd48640b876468354965f91e31a60dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Fern=C3=A1ndez=20Poyatos?= Date: Fri, 7 Feb 2025 13:29:25 +0100 Subject: [PATCH 19/30] docs: Add details about user creation in Prowler app (#6862) --- docs/index.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/index.md b/docs/index.md index f43bfb38ea..3ec79dd862 100644 --- a/docs/index.md +++ b/docs/index.md @@ -377,6 +377,19 @@ Go to [http://localhost:3000](http://localhost:3000) after installing the app (s Sign Up Button Sign Up +???+ note "User creation and default tenant behavior" + + When creating a new user, the behavior depends on whether an invitation is provided: + + - **Without an invitation**: + + - A new tenant is automatically created. + - The new user is assigned to this tenant. + - A set of **RBAC admin permissions** is generated and assigned to the user for the newly created tenant. + - **With an invitation**: The user is added to the specified tenant with the permissions defined in the invitation. + + This mechanism ensures that the first user in a newly created tenant has administrative permissions within that tenant. + #### **Log In** Log in with your email and password to start using the Prowler App. From 62cbff4f539e4c60b619f73fef40ace359b45558 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Fri, 7 Feb 2025 14:25:25 +0100 Subject: [PATCH 20/30] =?UTF-8?q?feat:=20implement=20new=20functionality?= =?UTF-8?q?=20with=20inserted=5Fat=5F=5Fgte=20in=20findings=20a=E2=80=A6?= =?UTF-8?q?=20(#6864)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ui/app/(prowler)/findings/page.tsx | 62 +++++++++++++++---- ui/app/(prowler)/page.tsx | 19 ++++-- ui/components/filters/custom-date-picker.tsx | 6 +- .../link-to-findings/link-to-findings.tsx | 2 +- 4 files changed, 69 insertions(+), 20 deletions(-) diff --git a/ui/app/(prowler)/findings/page.tsx b/ui/app/(prowler)/findings/page.tsx index 39d2ad7846..059438fd3f 100644 --- a/ui/app/(prowler)/findings/page.tsx +++ b/ui/app/(prowler)/findings/page.tsx @@ -1,4 +1,5 @@ import { Spacer } from "@nextui-org/react"; +import { format, subDays } from "date-fns"; import React, { Suspense } from "react"; import { getFindings, getMetadataInfo } from "@/actions/findings"; @@ -31,7 +32,21 @@ export default async function Findings({ // Make sure the sort is correctly encoded const encodedSort = sort?.replace(/^\+/, ""); + const twoDaysAgo = format(subDays(new Date(), 2), "yyyy-MM-dd"); + + // Check if the searchParams contain any date or scan filter + const hasDateOrScanFilter = Object.keys(searchParams).some( + (key) => key.includes("inserted_at") || key.includes("scan__in"), + ); + + // Default filters for getMetadataInfo + const defaultFilters: Record = hasDateOrScanFilter + ? {} // Do not apply default filters if there are date or scan filters + : { "filter[inserted_at__gte]": twoDaysAgo }; + + // Extract all filter parameters and combine with default filters const filters: Record = { + ...defaultFilters, ...Object.fromEntries( Object.entries(searchParams) .filter(([key]) => key.startsWith("filter[")) @@ -44,11 +59,15 @@ export default async function Findings({ const query = filters["filter[search]"] || ""; - const metadataInfoData = await getMetadataInfo({ - query, - sort: encodedSort, - filters, - }); + const [metadataInfoData, providersData, scansData] = await Promise.all([ + getMetadataInfo({ + query, + sort: encodedSort, + filters, + }), + getProviders({}), + getScans({}), + ]); // Extract unique regions and services from the new endpoint const uniqueRegions = metadataInfoData?.data?.attributes?.regions || []; @@ -56,8 +75,6 @@ export default async function Findings({ const uniqueResourceTypes = metadataInfoData?.data?.attributes?.resource_types || []; // Get findings data - const providersData = await getProviders({}); - const scansData = await getScans({}); // Extract provider UIDs const providerUIDs = Array.from( @@ -141,7 +158,20 @@ const SSRDataTable = async ({ // Make sure the sort is correctly encoded const encodedSort = sort.replace(/^\+/, ""); + const twoDaysAgo = format(subDays(new Date(), 2), "yyyy-MM-dd"); + + // Check if the searchParams contain any date or scan filter + const hasDateOrScanFilter = Object.keys(searchParams).some( + (key) => key.includes("inserted_at") || key.includes("scan__in"), + ); + + // Default filters for getFindings + const defaultFilters: Record = hasDateOrScanFilter + ? {} // Do not apply default filters if there are date or scan filters + : { "filter[inserted_at__gte]": twoDaysAgo }; + const filters: Record = { + ...defaultFilters, ...Object.fromEntries( Object.entries(searchParams) .filter(([key]) => key.startsWith("filter[")) @@ -190,10 +220,18 @@ const SSRDataTable = async ({ }; return ( - + <> + {findingsData?.errors && ( +
+

Error:

+

{findingsData.errors[0].detail}

+
+ )} + + ); }; diff --git a/ui/app/(prowler)/page.tsx b/ui/app/(prowler)/page.tsx index c6e0748362..e1b02c2bbe 100644 --- a/ui/app/(prowler)/page.tsx +++ b/ui/app/(prowler)/page.tsx @@ -1,4 +1,5 @@ import { Spacer } from "@nextui-org/react"; +import { format, subDays } from "date-fns"; import { Suspense } from "react"; import { getFindings } from "@/actions/findings/findings"; @@ -131,9 +132,12 @@ const SSRDataNewFindingsTable = async () => { const page = 1; const sort = "severity,-inserted_at"; + const twoDaysAgo = format(subDays(new Date(), 2), "yyyy-MM-dd"); + const defaultFilters = { "filter[status__in]": "FAIL", "filter[delta__in]": "new", + "filter[inserted_at__gte]": twoDaysAgo, }; const findingsData = await getFindings({ @@ -172,14 +176,21 @@ const SSRDataNewFindingsTable = async () => { return ( <> -
-

- Latest 10 failing findings to date by Severity -

+
+
+

+ Latest 10 failing findings to date by Severity +

+

+ Showing the latest 10 failing findings by severity from the last 2 + days. +

+
+ { const searchParams = useSearchParams(); const [value, setValue] = React.useState(() => { - const dateParam = searchParams.get("filter[updated_at]"); + const dateParam = searchParams.get("filter[inserted_at]"); return dateParam ? today(getLocalTimeZone()) : null; }); @@ -30,9 +30,9 @@ export const CustomDatePicker = () => { (date: any) => { const params = new URLSearchParams(searchParams.toString()); if (date) { - params.set("filter[updated_at]", date.toString()); + params.set("filter[inserted_at]", date.toString()); } else { - params.delete("filter[updated_at]"); + params.delete("filter[inserted_at]"); } router.push(`?${params.toString()}`, { scroll: false }); }, 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 2c86db8fd3..e5766db634 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 @@ -6,7 +6,7 @@ export const LinkToFindings = () => { return (
Date: Fri, 7 Feb 2025 14:48:16 +0100 Subject: [PATCH 21/30] fix(aws): key error for detect-secrets (#6710) --- ...lambda_function_no_secrets_in_variables.py | 13 +- ...task_definitions_no_environment_secrets.py | 13 +- ...a_function_no_secrets_in_variables_test.py | 47 +++- ...definitions_no_environment_secrets_test.py | 227 +++++++++++++++++- 4 files changed, 274 insertions(+), 26 deletions(-) diff --git a/prowler/providers/aws/services/awslambda/awslambda_function_no_secrets_in_variables/awslambda_function_no_secrets_in_variables.py b/prowler/providers/aws/services/awslambda/awslambda_function_no_secrets_in_variables/awslambda_function_no_secrets_in_variables.py index aea6617e16..9448b0239f 100644 --- a/prowler/providers/aws/services/awslambda/awslambda_function_no_secrets_in_variables/awslambda_function_no_secrets_in_variables.py +++ b/prowler/providers/aws/services/awslambda/awslambda_function_no_secrets_in_variables/awslambda_function_no_secrets_in_variables.py @@ -1,4 +1,3 @@ -import hashlib import json from prowler.lib.check.models import Check, Check_Report_AWS @@ -28,19 +27,13 @@ class awslambda_function_no_secrets_in_variables(Check): "detect_secrets_plugins", ), ) - original_env_vars = {} + original_env_vars = [] for name, value in function.environment.items(): - original_env_vars.update( - { - hashlib.sha1( # nosec B324 SHA1 is used here for non-security-critical unique identifiers - value.encode("utf-8") - ).hexdigest(): name - } - ) + original_env_vars.append(name) if detect_secrets_output: secrets_string = ", ".join( [ - f"{secret['type']} in variable {original_env_vars[secret['hashed_secret']]}" + f"{secret['type']} in variable {original_env_vars[secret['line_number'] - 2]}" for secret in detect_secrets_output ] ) diff --git a/prowler/providers/aws/services/ecs/ecs_task_definitions_no_environment_secrets/ecs_task_definitions_no_environment_secrets.py b/prowler/providers/aws/services/ecs/ecs_task_definitions_no_environment_secrets/ecs_task_definitions_no_environment_secrets.py index 42dc6b437c..cd835d0149 100644 --- a/prowler/providers/aws/services/ecs/ecs_task_definitions_no_environment_secrets/ecs_task_definitions_no_environment_secrets.py +++ b/prowler/providers/aws/services/ecs/ecs_task_definitions_no_environment_secrets/ecs_task_definitions_no_environment_secrets.py @@ -1,4 +1,3 @@ -import hashlib from json import dumps from prowler.lib.check.models import Check, Check_Report_AWS @@ -25,16 +24,10 @@ class ecs_task_definitions_no_environment_secrets(Check): if container.environment: dump_env_vars = {} - original_env_vars = {} + original_env_vars = [] for env_var in container.environment: dump_env_vars.update({env_var.name: env_var.value}) - original_env_vars.update( - { - hashlib.sha1( # nosec B324 SHA1 is used here for non-security-critical unique identifiers - env_var.value.encode("utf-8") - ).hexdigest(): env_var.name - } - ) + original_env_vars.append(env_var.name) env_data = dumps(dump_env_vars, indent=2) detect_secrets_output = detect_secrets_scan( @@ -47,7 +40,7 @@ class ecs_task_definitions_no_environment_secrets(Check): if detect_secrets_output: secrets_string = ", ".join( [ - f"{secret['type']} on the environment variable {original_env_vars[secret['hashed_secret']]}" + f"{secret['type']} on the environment variable {original_env_vars[secret['line_number'] - 2]}" for secret in detect_secrets_output ] ) diff --git a/tests/providers/aws/services/awslambda/awslambda_function_no_secrets_in_variables/awslambda_function_no_secrets_in_variables_test.py b/tests/providers/aws/services/awslambda/awslambda_function_no_secrets_in_variables/awslambda_function_no_secrets_in_variables_test.py index 432b31e927..bd38a225eb 100644 --- a/tests/providers/aws/services/awslambda/awslambda_function_no_secrets_in_variables/awslambda_function_no_secrets_in_variables_test.py +++ b/tests/providers/aws/services/awslambda/awslambda_function_no_secrets_in_variables/awslambda_function_no_secrets_in_variables_test.py @@ -76,7 +76,7 @@ class Test_awslambda_function_no_secrets_in_variables: ) assert result[0].resource_tags == [] - def test_function_secrets_in_variables(self): + def test_function_secrets_in_keyword(self): lambda_client = mock.MagicMock function_name = "test-lambda" function_runtime = "nodejs4.3" @@ -121,6 +121,51 @@ class Test_awslambda_function_no_secrets_in_variables: ) assert result[0].resource_tags == [] + def test_function_secrets_in_keyword_and_variable(self): + lambda_client = mock.MagicMock + function_name = "test-lambda" + function_runtime = "nodejs4.3" + function_arn = f"arn:aws:lambda:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:function/{function_name}" + + lambda_client.audit_config = {"secrets_ignore_patterns": []} + + lambda_client.functions = { + "function_name": Function( + name=function_name, + security_groups=[], + arn=function_arn, + region=AWS_REGION_US_EAST_1, + runtime=function_runtime, + environment={"db_password": "srv://admin:pass@db"}, + ) + } + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_aws_provider(), + ), mock.patch( + "prowler.providers.aws.services.awslambda.awslambda_function_no_secrets_in_variables.awslambda_function_no_secrets_in_variables.awslambda_client", + new=lambda_client, + ): + # Test Check + from prowler.providers.aws.services.awslambda.awslambda_function_no_secrets_in_variables.awslambda_function_no_secrets_in_variables import ( + awslambda_function_no_secrets_in_variables, + ) + + check = awslambda_function_no_secrets_in_variables() + result = check.execute() + + assert len(result) == 1 + assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_id == function_name + assert result[0].resource_arn == function_arn + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Potential secret found in Lambda function {function_name} variables -> Secret Keyword in variable db_password, Basic Auth Credentials in variable db_password." + ) + assert result[0].resource_tags == [] + def test_function_secrets_in_variables_telegram_token(self): lambda_client = mock.MagicMock function_name = "test-lambda" diff --git a/tests/providers/aws/services/ecs/ecs_task_definitions_no_environment_secrets/ecs_task_definitions_no_environment_secrets_test.py b/tests/providers/aws/services/ecs/ecs_task_definitions_no_environment_secrets/ecs_task_definitions_no_environment_secrets_test.py index 18dc007713..f6b5c411c2 100644 --- a/tests/providers/aws/services/ecs/ecs_task_definitions_no_environment_secrets/ecs_task_definitions_no_environment_secrets_test.py +++ b/tests/providers/aws/services/ecs/ecs_task_definitions_no_environment_secrets/ecs_task_definitions_no_environment_secrets_test.py @@ -2,7 +2,6 @@ from unittest.mock import patch from boto3 import client from moto import mock_aws - from tests.providers.aws.utils import AWS_REGION_US_EAST_1, set_mocked_aws_provider TASK_NAME = "test-task" @@ -10,8 +9,10 @@ TASK_REVISION = "1" CONTAINER_NAME = "test-container" ENV_VAR_NAME_NO_SECRETS = "host" ENV_VAR_VALUE_NO_SECRETS = "localhost:1234" -ENV_VAR_NAME_WITH_SECRETS = "DB_PASSWORD" -ENV_VAR_VALUE_WITH_SECRETS = "pass-12343" +ENV_VAR_NAME_WITH_KEYWORD = "DB_PASSWORD" +ENV_VAR_VALUE_WITH_SECRETS = "srv://admin:pass@db" +ENV_VAR_NAME_WITH_KEYWORD2 = "DATABASE_PASSWORD" +ENV_VAR_VALUE_WITH_SECRETS2 = "srv://admin:password@database" class Test_ecs_task_definitions_no_environment_secrets: @@ -88,7 +89,7 @@ class Test_ecs_task_definitions_no_environment_secrets: assert result[0].resource_tags == [] @mock_aws - def test_container_env_var_with_secrets(self): + def test_container_env_var_with_secret(self): ecs_client = client("ecs", region_name=AWS_REGION_US_EAST_1) task_arn = ecs_client.register_task_definition( @@ -103,7 +104,7 @@ class Test_ecs_task_definitions_no_environment_secrets: "user": "appuser", "environment": [ { - "name": ENV_VAR_NAME_WITH_SECRETS, + "name": ENV_VAR_NAME_NO_SECRETS, "value": ENV_VAR_VALUE_WITH_SECRETS, } ], @@ -115,6 +116,58 @@ class Test_ecs_task_definitions_no_environment_secrets: mocked_aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + with patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mocked_aws_provider, + ), patch( + "prowler.providers.aws.services.ecs.ecs_task_definitions_no_environment_secrets.ecs_task_definitions_no_environment_secrets.ecs_client", + new=ECS(mocked_aws_provider), + ): + from prowler.providers.aws.services.ecs.ecs_task_definitions_no_environment_secrets.ecs_task_definitions_no_environment_secrets import ( + ecs_task_definitions_no_environment_secrets, + ) + + check = ecs_task_definitions_no_environment_secrets() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Potential secrets found in ECS task definition {TASK_NAME} with revision {TASK_REVISION}: Secrets in container test-container -> Basic Auth Credentials on the environment variable host." + ) + assert result[0].resource_id == f"{TASK_NAME}:{TASK_REVISION}" + assert result[0].resource_arn == task_arn + assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_tags == [] + + @mock_aws + def test_container_env_var_with_keyword(self): + ecs_client = client("ecs", region_name=AWS_REGION_US_EAST_1) + + task_arn = ecs_client.register_task_definition( + family=TASK_NAME, + containerDefinitions=[ + { + "name": CONTAINER_NAME, + "image": "ubuntu", + "memory": 128, + "readonlyRootFilesystem": True, + "privileged": False, + "user": "appuser", + "environment": [ + { + "name": ENV_VAR_NAME_WITH_KEYWORD, + "value": ENV_VAR_VALUE_NO_SECRETS, + } + ], + } + ], + )["taskDefinition"]["taskDefinitionArn"] + + from prowler.providers.aws.services.ecs.ecs_service import ECS + + mocked_aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + with patch( "prowler.providers.common.provider.Provider.get_global_provider", return_value=mocked_aws_provider, @@ -138,3 +191,167 @@ class Test_ecs_task_definitions_no_environment_secrets: assert result[0].resource_arn == task_arn assert result[0].region == AWS_REGION_US_EAST_1 assert result[0].resource_tags == [] + + @mock_aws + def test_container_env_var_with_keyword_and_secret(self): + ecs_client = client("ecs", region_name=AWS_REGION_US_EAST_1) + + task_arn = ecs_client.register_task_definition( + family=TASK_NAME, + containerDefinitions=[ + { + "name": CONTAINER_NAME, + "image": "ubuntu", + "memory": 128, + "readonlyRootFilesystem": True, + "privileged": False, + "user": "appuser", + "environment": [ + { + "name": ENV_VAR_NAME_WITH_KEYWORD, + "value": ENV_VAR_VALUE_WITH_SECRETS, + } + ], + } + ], + )["taskDefinition"]["taskDefinitionArn"] + + from prowler.providers.aws.services.ecs.ecs_service import ECS + + mocked_aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mocked_aws_provider, + ), patch( + "prowler.providers.aws.services.ecs.ecs_task_definitions_no_environment_secrets.ecs_task_definitions_no_environment_secrets.ecs_client", + new=ECS(mocked_aws_provider), + ): + from prowler.providers.aws.services.ecs.ecs_task_definitions_no_environment_secrets.ecs_task_definitions_no_environment_secrets import ( + ecs_task_definitions_no_environment_secrets, + ) + + check = ecs_task_definitions_no_environment_secrets() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Potential secrets found in ECS task definition {TASK_NAME} with revision {TASK_REVISION}: Secrets in container test-container -> Secret Keyword on the environment variable DB_PASSWORD, Basic Auth Credentials on the environment variable DB_PASSWORD." + ) + assert result[0].resource_id == f"{TASK_NAME}:{TASK_REVISION}" + assert result[0].resource_arn == task_arn + assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_tags == [] + + @mock_aws + def test_container_multiple_env_vars_with_keyword_and_secret(self): + ecs_client = client("ecs", region_name=AWS_REGION_US_EAST_1) + + task_arn = ecs_client.register_task_definition( + family=TASK_NAME, + containerDefinitions=[ + { + "name": CONTAINER_NAME, + "image": "ubuntu", + "memory": 128, + "readonlyRootFilesystem": True, + "privileged": False, + "user": "appuser", + "environment": [ + { + "name": ENV_VAR_NAME_WITH_KEYWORD, + "value": ENV_VAR_VALUE_WITH_SECRETS, + }, + { + "name": ENV_VAR_NAME_NO_SECRETS, + "value": ENV_VAR_VALUE_WITH_SECRETS2, + }, + ], + } + ], + )["taskDefinition"]["taskDefinitionArn"] + + from prowler.providers.aws.services.ecs.ecs_service import ECS + + mocked_aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mocked_aws_provider, + ), patch( + "prowler.providers.aws.services.ecs.ecs_task_definitions_no_environment_secrets.ecs_task_definitions_no_environment_secrets.ecs_client", + new=ECS(mocked_aws_provider), + ): + from prowler.providers.aws.services.ecs.ecs_task_definitions_no_environment_secrets.ecs_task_definitions_no_environment_secrets import ( + ecs_task_definitions_no_environment_secrets, + ) + + check = ecs_task_definitions_no_environment_secrets() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Potential secrets found in ECS task definition {TASK_NAME} with revision {TASK_REVISION}: Secrets in container test-container -> Secret Keyword on the environment variable DB_PASSWORD, Basic Auth Credentials on the environment variable DB_PASSWORD, Basic Auth Credentials on the environment variable host." + ) + assert result[0].resource_id == f"{TASK_NAME}:{TASK_REVISION}" + assert result[0].resource_arn == task_arn + assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_tags == [] + + @mock_aws + def test_container_all_env_vars_with_keyword_and_secret(self): + ecs_client = client("ecs", region_name=AWS_REGION_US_EAST_1) + + task_arn = ecs_client.register_task_definition( + family=TASK_NAME, + containerDefinitions=[ + { + "name": CONTAINER_NAME, + "image": "ubuntu", + "memory": 128, + "readonlyRootFilesystem": True, + "privileged": False, + "user": "appuser", + "environment": [ + { + "name": ENV_VAR_NAME_WITH_KEYWORD, + "value": ENV_VAR_VALUE_WITH_SECRETS, + }, + { + "name": ENV_VAR_NAME_WITH_KEYWORD2, + "value": ENV_VAR_VALUE_WITH_SECRETS2, + }, + ], + } + ], + )["taskDefinition"]["taskDefinitionArn"] + + from prowler.providers.aws.services.ecs.ecs_service import ECS + + mocked_aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mocked_aws_provider, + ), patch( + "prowler.providers.aws.services.ecs.ecs_task_definitions_no_environment_secrets.ecs_task_definitions_no_environment_secrets.ecs_client", + new=ECS(mocked_aws_provider), + ): + from prowler.providers.aws.services.ecs.ecs_task_definitions_no_environment_secrets.ecs_task_definitions_no_environment_secrets import ( + ecs_task_definitions_no_environment_secrets, + ) + + check = ecs_task_definitions_no_environment_secrets() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Potential secrets found in ECS task definition {TASK_NAME} with revision {TASK_REVISION}: Secrets in container test-container -> Secret Keyword on the environment variable DB_PASSWORD, Basic Auth Credentials on the environment variable DB_PASSWORD, Basic Auth Credentials on the environment variable DATABASE_PASSWORD, Secret Keyword on the environment variable DATABASE_PASSWORD." + ) + assert result[0].resource_id == f"{TASK_NAME}:{TASK_REVISION}" + assert result[0].resource_arn == task_arn + assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_tags == [] From 377faf145f6cd4cc99cd58339f293babd241da11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Fern=C3=A1ndez=20Poyatos?= Date: Fri, 7 Feb 2025 14:51:01 +0100 Subject: [PATCH 22/30] feat(findings): Use ArrayAgg and subqueries on metadata endpoint (#6863) Co-authored-by: Pepe Fagoaga --- api/src/backend/api/CHANGELOG.md | 1 + api/src/backend/api/v1/views.py | 54 +++++++++----------------------- 2 files changed, 16 insertions(+), 39 deletions(-) diff --git a/api/src/backend/api/CHANGELOG.md b/api/src/backend/api/CHANGELOG.md index 6f8971f40d..07b5e3f8ff 100644 --- a/api/src/backend/api/CHANGELOG.md +++ b/api/src/backend/api/CHANGELOG.md @@ -10,5 +10,6 @@ All notable changes to the **Prowler API** are documented in this file. ### Changed - Daily scheduled scan instances are now created beforehand with `SCHEDULED` state [(#6700)](https://github.com/prowler-cloud/prowler/pull/6700). - Findings endpoints now require at least one date filter [(#6800)](https://github.com/prowler-cloud/prowler/pull/6800). +- Findings metadata endpoint received a performance improvement [(#6863)](https://github.com/prowler-cloud/prowler/pull/6863). --- diff --git a/api/src/backend/api/v1/views.py b/api/src/backend/api/v1/views.py index 13c1d43f23..722ecbfd7d 100644 --- a/api/src/backend/api/v1/views.py +++ b/api/src/backend/api/v1/views.py @@ -1418,50 +1418,26 @@ class FindingViewSet(BaseRLSViewSet): queryset = self.get_queryset() filtered_queryset = self.filter_queryset(queryset) - relevant_resources = Resource.objects.filter( - tenant_id=tenant_id, findings__in=filtered_queryset - ).distinct() + filtered_ids = filtered_queryset.order_by().values("id") - services = ( - relevant_resources.values_list("service", flat=True) - .distinct() - .order_by("service") + relevant_resources = Resource.all_objects.filter( + tenant_id=tenant_id, findings__id__in=Subquery(filtered_ids) + ).only("service", "region", "type") + + aggregation = relevant_resources.aggregate( + services=ArrayAgg("service", flat=True), + regions=ArrayAgg("region", flat=True), + resource_types=ArrayAgg("type", flat=True), ) - regions = ( - relevant_resources.exclude(region="") - .values_list("region", flat=True) - .distinct() - .order_by("region") - ) - - resource_types = ( - relevant_resources.values_list("type", flat=True) - .distinct() - .order_by("type") - ) - - # Temporarily disabled until we implement tag filtering in the UI - # tag_data = ( - # relevant_resources - # .filter(tags__key__isnull=False, tags__value__isnull=False) - # .exclude(tags__key="") - # .exclude(tags__value="") - # .values("tags__key", "tags__value") - # .distinct() - # .order_by("tags__key", "tags__value") - # ) - # - # tags_dict = {} - # for row in tag_data: - # k, v = row["tags__key"], row["tags__value"] - # tags_dict.setdefault(k, []).append(v) + services = sorted(set(aggregation["services"] or [])) + regions = sorted({region for region in aggregation["regions"] or [] if region}) + resource_types = sorted(set(aggregation["resource_types"] or [])) result = { - "services": list(services), - "regions": list(regions), - "resource_types": list(resource_types), - # "tags": tags_dict + "services": services, + "regions": regions, + "resource_types": resource_types, } serializer = self.get_serializer(data=result) From 54b82a78e3917d6e57bf749680e6bbbe1f193ac4 Mon Sep 17 00:00:00 2001 From: Prowler Bot Date: Fri, 7 Feb 2025 16:02:28 +0100 Subject: [PATCH 23/30] chore(regions_update): Changes in regions for AWS services (#6858) Co-authored-by: MrCloudSec <38561120+MrCloudSec@users.noreply.github.com> --- prowler/providers/aws/aws_regions_by_service.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/prowler/providers/aws/aws_regions_by_service.json b/prowler/providers/aws/aws_regions_by_service.json index 49e1c23665..04fd6e3efc 100644 --- a/prowler/providers/aws/aws_regions_by_service.json +++ b/prowler/providers/aws/aws_regions_by_service.json @@ -7950,6 +7950,7 @@ "ap-southeast-3", "ap-southeast-4", "ca-central-1", + "ca-west-1", "eu-central-1", "eu-central-2", "eu-north-1", @@ -7958,6 +7959,7 @@ "eu-west-1", "eu-west-2", "eu-west-3", + "il-central-1", "me-central-1", "me-south-1", "sa-east-1", From f628897fe1f5dda2cf12f2bb28ec24c997b9a71b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Mart=C3=ADn?= Date: Fri, 7 Feb 2025 16:05:30 +0100 Subject: [PATCH 24/30] fix(dashboard): adjust the bar chart display (#6690) --- dashboard/pages/compliance.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dashboard/pages/compliance.py b/dashboard/pages/compliance.py index ce47ac1e8c..a1198d777d 100644 --- a/dashboard/pages/compliance.py +++ b/dashboard/pages/compliance.py @@ -532,8 +532,8 @@ def get_bar_graph(df, column_name): # Cut the text if it is too long for i in range(len(colums)): - if len(colums[i]) > 15: - colums[i] = colums[i][:15] + "..." + if len(colums[i]) > 43: + colums[i] = colums[i][:43] + "..." fig = px.bar( df, From b629bc81f86883250d24a2bfd21f0da44ac265be Mon Sep 17 00:00:00 2001 From: Sergio Garcia Date: Fri, 7 Feb 2025 10:59:01 -0500 Subject: [PATCH 25/30] docs(eks): add documentation about EKS onboarding (#6853) Co-authored-by: Pepe Fagoaga --- docs/index.md | 1 + docs/tutorials/kubernetes/in-cluster.md | 1 + docs/tutorials/prowler-app.md | 26 +++++++++++++++++++ kubernetes/job.yaml | 13 ---------- kubernetes/prowler-sa.yaml | 10 +++++++ .../kubernetes/services/rbac/rbac_service.py | 2 +- 6 files changed, 39 insertions(+), 14 deletions(-) create mode 100644 kubernetes/prowler-sa.yaml diff --git a/docs/index.md b/docs/index.md index 3ec79dd862..648564bc18 100644 --- a/docs/index.md +++ b/docs/index.md @@ -548,6 +548,7 @@ prowler kubernetes --kubeconfig-file path For in-cluster execution, you can use the supplied yaml to run Prowler as a job within a new Prowler namespace: ```console +kubectl apply -f kubernetes/prowler-sa.yaml kubectl apply -f kubernetes/job.yaml kubectl apply -f kubernetes/prowler-role.yaml kubectl apply -f kubernetes/prowler-rolebinding.yaml diff --git a/docs/tutorials/kubernetes/in-cluster.md b/docs/tutorials/kubernetes/in-cluster.md index cf401f1ff6..ae733db84a 100644 --- a/docs/tutorials/kubernetes/in-cluster.md +++ b/docs/tutorials/kubernetes/in-cluster.md @@ -9,6 +9,7 @@ For in-cluster execution, you can use the supplied yaml files inside `/kubernete They can be used to run Prowler as a job within a new Prowler namespace: ```console +kubectl apply -f kubernetes/prowler-sa.yaml kubectl apply -f kubernetes/job.yaml kubectl apply -f kubernetes/prowler-role.yaml kubectl apply -f kubernetes/prowler-rolebinding.yaml diff --git a/docs/tutorials/prowler-app.md b/docs/tutorials/prowler-app.md index 3184d9e958..d4d3d2f890 100644 --- a/docs/tutorials/prowler-app.md +++ b/docs/tutorials/prowler-app.md @@ -99,6 +99,32 @@ By default, the `kubeconfig` file is located at `~/.kube/config`. Kubernetes Credentials +???+ note + If you are adding an **Amazon EKS** cluster, follow these additional steps to ensure proper authentication: + + 1. Apply the necessary Kubernetes resources to your EKS cluster (you can find the files in the [`kubernetes` directory of the Prowler repository](https://github.com/prowler-cloud/prowler/tree/master/kubernetes)): + ```console + kubectl apply -f kubernetes/prowler-sa.yaml + kubectl apply -f kubernetes/prowler-role.yaml + kubectl apply -f kubernetes/prowler-rolebinding.yaml + ``` + + 2. Generate a long-lived token for authentication: + ```console + kubectl create token prowler-sa -n prowler-ns --duration=0 + ``` + - **Security Note:** The `--duration=0` option generates a non-expiring token, which may pose a security risk if not managed properly. Users should decide on an appropriate expiration time based on their security policies. If a limited-time token is preferred, set `--duration=