From 69d3a9e36346080f2d829d41b3f279d264514e9f Mon Sep 17 00:00:00 2001 From: Pepe Fagoaga Date: Mon, 7 Nov 2022 16:17:38 +0100 Subject: [PATCH] feat(cloudformation): Service and Checks (#1454) Co-authored-by: Sergio Garcia <38561120+sergargar@users.noreply.github.com> --- Pipfile | 2 +- Pipfile.lock | 202 ++++++++++++++--- .../aws/services/cloudformation/__init__.py | 0 .../services/cloudformation/check_extra7154 | 60 ----- .../services/cloudformation/check_extra742 | 75 ------ .../cloudformation/cloudformation_client.py | 4 + .../__init__.py | 0 ...rmation_outputs_find_secrets.metadata.json | 35 +++ .../cloudformation_outputs_find_secrets.py | 57 +++++ ...loudformation_outputs_find_secrets_test.py | 133 +++++++++++ .../cloudformation/cloudformation_service.py | 108 +++++++++ .../cloudformation_service_test.py | 213 ++++++++++++++++++ .../__init__.py | 0 ...rmination_protection_enabled.metadata.json | 35 +++ ...n_stacks_termination_protection_enabled.py | 28 +++ ...cks_termination_protection_enabled_test.py | 99 ++++++++ 16 files changed, 880 insertions(+), 171 deletions(-) create mode 100644 providers/aws/services/cloudformation/__init__.py delete mode 100644 providers/aws/services/cloudformation/check_extra7154 delete mode 100644 providers/aws/services/cloudformation/check_extra742 create mode 100644 providers/aws/services/cloudformation/cloudformation_client.py create mode 100644 providers/aws/services/cloudformation/cloudformation_outputs_find_secrets/__init__.py create mode 100644 providers/aws/services/cloudformation/cloudformation_outputs_find_secrets/cloudformation_outputs_find_secrets.metadata.json create mode 100644 providers/aws/services/cloudformation/cloudformation_outputs_find_secrets/cloudformation_outputs_find_secrets.py create mode 100644 providers/aws/services/cloudformation/cloudformation_outputs_find_secrets/cloudformation_outputs_find_secrets_test.py create mode 100644 providers/aws/services/cloudformation/cloudformation_service.py create mode 100644 providers/aws/services/cloudformation/cloudformation_service_test.py create mode 100644 providers/aws/services/cloudformation/cloudformation_stacks_termination_protection_enabled/__init__.py create mode 100644 providers/aws/services/cloudformation/cloudformation_stacks_termination_protection_enabled/cloudformation_stacks_termination_protection_enabled.metadata.json create mode 100644 providers/aws/services/cloudformation/cloudformation_stacks_termination_protection_enabled/cloudformation_stacks_termination_protection_enabled.py create mode 100644 providers/aws/services/cloudformation/cloudformation_stacks_termination_protection_enabled/cloudformation_stacks_termination_protection_enabled_test.py diff --git a/Pipfile b/Pipfile index b3fe11f5..554469c8 100644 --- a/Pipfile +++ b/Pipfile @@ -5,7 +5,7 @@ name = "pypi" [packages] colorama = "0.4.4" -boto3 = "1.24.8" +boto3 = "1.26.3" arnparse = "0.0.2" botocore = "1.27.8" pydantic = "1.9.1" diff --git a/Pipfile.lock b/Pipfile.lock index eca2ef87..6a416c14 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "17d457477683f58bd28bc11042edcc0374d620d037243490cc3c7803cb5a4536" + "sha256": "7b5d26e522da612a9192a5d5122e2a100162936290013fd0053d7a14da41e422" }, "pipfile-spec": 6, "requires": { @@ -42,26 +42,26 @@ }, "boto3": { "hashes": [ - "sha256:0a19d07a39d69b8e84e24d75474bbf4e737b1749d0c665503dfc2446f321e1f0", - "sha256:8f0e4eb5c26f927c09bc03302d38156af578b816f1e4f8ca4f0f734d134b9d4f" + "sha256:7e871c481f88e5b2fc6ac16eb190c95de21efb43ab2d959beacf8b7b096b11d2", + "sha256:b81e4aa16891eac7532ce6cc9eb690a8d2e0ceea3bcf44b5c5a1309c2500d35f" ], "index": "pypi", - "version": "==1.26.0" + "version": "==1.26.3" }, "botocore": { "hashes": [ - "sha256:c706640f8cf9297d2af87fc711394631afc14aaa225c7554e220964b5047b47d", - "sha256:f25dc0827005f81abf4b890a20c71f64ee40ea9e9795df38a242fdeed79e0a89" + "sha256:100534532b2745f6fa019b79199a8941f04b8168f9d557d0847191455f1f1eed", + "sha256:ac7986fefe1b9c6323d381c4fdee3845c67fa53eb6c9cf586a8e8a07270dbcfe" ], "index": "pypi", - "version": "==1.29.0" + "version": "==1.29.3" }, "certifi": { "hashes": [ "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14", "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382" ], - "markers": "python_full_version >= '3.6.0'", + "markers": "python_version >= '3.6'", "version": "==2022.9.24" }, "cffi": { @@ -138,7 +138,7 @@ "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845", "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f" ], - "markers": "python_full_version >= '3.6.0'", + "markers": "python_version >= '3.6'", "version": "==2.1.1" }, "click": { @@ -146,7 +146,7 @@ "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e", "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48" ], - "markers": "python_version >= '3.7'", + "markers": "python_full_version >= '3.7.0'", "version": "==8.1.3" }, "click-plugins": { @@ -249,9 +249,25 @@ "sha256:dfb4f4dd568de1b6af9f4cda334adf7d72cf5bc052516e1b2608b683375dd95c", "sha256:ed7b00096790213e09eb11c97cc6e2b757f15f3d2f85833cd2d3ec3fe37c1722" ], - "markers": "python_full_version >= '3.6.0'", + "markers": "python_version >= '3.6'", "version": "==38.0.3" }, + "detect-secrets": { + "hashes": [ + "sha256:d08ecabeee8b68c0acb0e8a354fb98d822a653f6ed05e520cead4c6fc1fc02cd", + "sha256:d56787e339758cef48c9ccd6692f7a094b9963c979c9813580b0169e41132833" + ], + "index": "pypi", + "version": "==1.4.0" + }, + "docker": { + "hashes": [ + "sha256:896c4282e5c7af5c45e8b683b0b0c33932974fe6e50fc6906a0a83616ab3da97", + "sha256:dbcb3bd2fa80dca0788ed908218bf43972772009b881ed1e20dfc29a65e49782" + ], + "index": "pypi", + "version": "==6.0.1" + }, "dparse": { "hashes": [ "sha256:8097076f1dd26c377f30d4745e6ec18fef42f3bf493933b842ac5bafad8c345f", @@ -262,11 +278,11 @@ }, "exceptiongroup": { "hashes": [ - "sha256:2ac84b496be68464a2da60da518af3785fff8b7ec0d090a581604bc870bdee41", - "sha256:affbabf13fb6e98988c38d9c5650e701569fe3c1de3233cfb61c5f33774690ad" + "sha256:4d6c0aa6dd825810941c792f53d7b8d71da26f5e5f84f20f9508e8f2d33b140a", + "sha256:73866f7f842ede6cb1daa42c4af078e2035e5f7607f0e2c762cc51bb31bbe7b2" ], "markers": "python_version < '3.11'", - "version": "==1.0.0" + "version": "==1.0.1" }, "execnet": { "hashes": [ @@ -281,7 +297,7 @@ "sha256:8033ad4e853066ba6ca92050b9df2f89301b8fc8bf7e9324d412a63f8bf1a8fd", "sha256:bac2fd45c0a1c9cf619e63a90d62bdc63892ef92387424b855792a6cabe789aa" ], - "markers": "python_full_version >= '3.6.0'", + "markers": "python_version >= '3.6'", "version": "==4.0.9" }, "gitpython": { @@ -289,7 +305,7 @@ "sha256:41eea0deec2deea139b459ac03656f0dd28fc4a3387240ec1d3c259a2c47850f", "sha256:cc36bfc4a3f913e66805a28e84703e419d9c264c1077e537b54f0e1af85dbefd" ], - "markers": "python_version >= '3.7'", + "markers": "python_full_version >= '3.7.0'", "version": "==3.1.29" }, "idna": { @@ -300,6 +316,14 @@ "markers": "python_version >= '3.5'", "version": "==3.4" }, + "importlib-resources": { + "hashes": [ + "sha256:c01b1b94210d9849f286b86bb51bcea7cd56dde0600d8db721d7b81330711668", + "sha256:ee17ec648f85480d523596ce49eae8ead87d5631ae1551f913c0100b5edd3437" + ], + "markers": "python_full_version >= '3.7.0'", + "version": "==5.10.0" + }, "iniconfig": { "hashes": [ "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", @@ -312,7 +336,7 @@ "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852", "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61" ], - "markers": "python_version >= '3.7'", + "markers": "python_full_version >= '3.7.0'", "version": "==3.1.2" }, "jmespath": { @@ -320,9 +344,50 @@ "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe" ], - "markers": "python_version >= '3.7'", + "markers": "python_full_version >= '3.7.0'", "version": "==1.0.1" }, + "jsonschema": { + "hashes": [ + "sha256:5bfcf2bca16a087ade17e02b282d34af7ccd749ef76241e7f9bd7c0cb8a9424d", + "sha256:f660066c3966db7d6daeaea8a75e0b68237a48e51cf49882087757bb59916248" + ], + "markers": "python_full_version >= '3.7.0'", + "version": "==4.17.0" + }, + "jsonschema-spec": { + "hashes": [ + "sha256:1e525177574c23ae0f55cd62382632a083a0339928f0ca846a975a4da9851cec", + "sha256:780a22d517cdc857d9714a80d8349c546945063f20853ea32ba7f85bc643ec7d" + ], + "markers": "python_full_version >= '3.7.0' and python_full_version < '4.0.0'", + "version": "==0.1.2" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0c1c7c0433154bb7c54185714c6929acc0ba04ee1b167314a779b9025517eada", + "sha256:14010b49a2f56ec4943b6cf925f597b534ee2fe1f0738c84b3bce0c1a11ff10d", + "sha256:4e2d9f764f1befd8bdc97673261b8bb888764dfdbd7a4d8f55e4fbcabb8c3fb7", + "sha256:4fd031589121ad46e293629b39604031d354043bb5cdf83da4e93c2d7f3389fe", + "sha256:5b51d6f3bfeb289dfd4e95de2ecd464cd51982fe6f00e2be1d0bf94864d58acd", + "sha256:6850e4aeca6d0df35bb06e05c8b934ff7c533734eb51d0ceb2d63696f1e6030c", + "sha256:6f593f26c470a379cf7f5bc6db6b5f1722353e7bf937b8d0d0b3fba911998858", + "sha256:71d9ae8a82203511a6f60ca5a1b9f8ad201cac0fc75038b2dc5fa519589c9288", + "sha256:7e1561626c49cb394268edd00501b289053a652ed762c58e1081224c8d881cec", + "sha256:8f6ce2118a90efa7f62dd38c7dbfffd42f468b180287b748626293bf12ed468f", + "sha256:ae032743794fba4d171b5b67310d69176287b5bf82a21f588282406a79498891", + "sha256:afcaa24e48bb23b3be31e329deb3f1858f1f1df86aea3d70cb5c8578bfe5261c", + "sha256:b70d6e7a332eb0217e7872a73926ad4fdc14f846e85ad6749ad111084e76df25", + "sha256:c219a00245af0f6fa4e95901ed28044544f50152840c5b6a3e7b2568db34d156", + "sha256:ce58b2b3734c73e68f0e30e4e725264d4d6be95818ec0a0be4bb6bf9a7e79aa8", + "sha256:d176f392dbbdaacccf15919c77f526edf11a34aece58b55ab58539807b85436f", + "sha256:e20bfa6db17a39c706d24f82df8352488d2943a3b7ce7d4c22579cb89ca8896e", + "sha256:eac3a9a5ef13b332c059772fd40b4b1c3d45a3a2b05e33a361dee48e54a4dad0", + "sha256:eb329f8d8145379bf5dbe722182410fe8863d186e51bf034d2075eb8d85ee25b" + ], + "markers": "python_full_version >= '3.7.0'", + "version": "==1.8.0" + }, "markupsafe": { "hashes": [ "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003", @@ -366,7 +431,7 @@ "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a", "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7" ], - "markers": "python_version >= '3.7'", + "markers": "python_full_version >= '3.7.0'", "version": "==2.1.1" }, "mock": { @@ -374,11 +439,10 @@ "sha256:122fcb64ee37cfad5b3f48d7a7d51875d7031aaf3d8be7c42e2bee25044eee62", "sha256:7d3fbbde18228f4ff2f1f119a45cdffa458b4c0dee32eb4d2bb2f82554bac7bc" ], - "markers": "python_full_version >= '3.6.0'", + "markers": "python_version >= '3.6'", "version": "==4.0.3" }, "moto": { - "extras": [], "hashes": [ "sha256:2fb909d2ea1b732f89604e4268e2c2207c253e590a635a410c3c2aaebb34e113", "sha256:ba03b638cf3b1cec64cbe9ac0d184ca898b69020c8e3c5b9b4961c1670629010" @@ -386,14 +450,38 @@ "index": "pypi", "version": "==4.0.9" }, + "openapi-schema-validator": { + "hashes": [ + "sha256:34fbd14b7501abe25e64d7b4624a9db02cde1a578d285b3da6f34b290cdf0b3a", + "sha256:7cf27585dd7970b7257cefe48e1a3a10d4e34421831bdb472d96967433bc27bd" + ], + "markers": "python_full_version >= '3.7.0' and python_full_version < '4.0.0'", + "version": "==0.3.4" + }, + "openapi-spec-validator": { + "hashes": [ + "sha256:4a8aee1e45b1ac868e07ab25e18828fe9837baddd29a8e20fdb3d3c61c8eea3d", + "sha256:8248634bad1f23cac5d5a34e193ab36e23914057ca69e91a1ede5af75552c465" + ], + "index": "pypi", + "version": "==0.5.1" + }, "packaging": { "hashes": [ "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" ], - "markers": "python_full_version >= '3.6.0'", + "markers": "python_version >= '3.6'", "version": "==21.3" }, + "pathable": { + "hashes": [ + "sha256:5c869d315be50776cc8a993f3af43e0c60dc01506b399643f919034ebf4cdcab", + "sha256:cdd7b1f9d7d5c8b8d3315dbf5a86b2596053ae845f056f57d97c0eefff84da14" + ], + "markers": "python_full_version >= '3.7.0' and python_full_version < '4.0.0'", + "version": "==0.4.3" + }, "pbr": { "hashes": [ "sha256:b97bc6695b2aff02144133c2e7399d5885223d42b7912ffaec2ca3898e673bfe", @@ -407,7 +495,7 @@ "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" ], - "markers": "python_full_version >= '3.6.0'", + "markers": "python_version >= '3.6'", "version": "==1.0.0" }, "pycparser": { @@ -467,6 +555,34 @@ "markers": "python_full_version >= '3.6.8'", "version": "==3.0.9" }, + "pyrsistent": { + "hashes": [ + "sha256:055ab45d5911d7cae397dc418808d8802fb95262751872c841c170b0dbf51eed", + "sha256:111156137b2e71f3a9936baf27cb322e8024dac3dc54ec7fb9f0bcf3249e68bb", + "sha256:187d5730b0507d9285a96fca9716310d572e5464cadd19f22b63a6976254d77a", + "sha256:21455e2b16000440e896ab99e8304617151981ed40c29e9507ef1c2e4314ee95", + "sha256:2aede922a488861de0ad00c7630a6e2d57e8023e4be72d9d7147a9fcd2d30712", + "sha256:3ba4134a3ff0fc7ad225b6b457d1309f4698108fb6b35532d015dca8f5abed73", + "sha256:456cb30ca8bff00596519f2c53e42c245c09e1a4543945703acd4312949bfd41", + "sha256:71d332b0320642b3261e9fee47ab9e65872c2bd90260e5d225dabeed93cbd42b", + "sha256:879b4c2f4d41585c42df4d7654ddffff1239dc4065bc88b745f0341828b83e78", + "sha256:9cd3e9978d12b5d99cbdc727a3022da0430ad007dacf33d0bf554b96427f33ab", + "sha256:a178209e2df710e3f142cbd05313ba0c5ebed0a55d78d9945ac7a4e09d923308", + "sha256:b39725209e06759217d1ac5fcdb510e98670af9e37223985f330b611f62e7425", + "sha256:bfa0351be89c9fcbcb8c9879b826f4353be10f58f8a677efab0c017bf7137ec2", + "sha256:bfd880614c6237243ff53a0539f1cb26987a6dc8ac6e66e0c5a40617296a045e", + "sha256:c43bec251bbd10e3cb58ced80609c5c1eb238da9ca78b964aea410fb820d00d6", + "sha256:d690b18ac4b3e3cab73b0b7aa7dbe65978a172ff94970ff98d82f2031f8971c2", + "sha256:d6982b5a0237e1b7d876b60265564648a69b14017f3b5f908c5be2de3f9abb7a", + "sha256:dec3eac7549869365fe263831f576c8457f6c833937c68542d08fde73457d291", + "sha256:e371b844cec09d8dc424d940e54bba8f67a03ebea20ff7b7b0d56f526c71d584", + "sha256:e5d8f84d81e3729c3b506657dddfe46e8ba9c330bf1858ee33108f8bb2adb38a", + "sha256:ea6b79a02a28550c98b6ca9c35b9f492beaa54d7c5c9e9949555893c8a9234d0", + "sha256:f1258f4e6c42ad0b20f9cfcc3ada5bd6b83374516cd01c0960e3cb75fdca6770" + ], + "markers": "python_full_version >= '3.7.0'", + "version": "==0.19.2" + }, "pytest": { "hashes": [ "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71", @@ -541,7 +657,7 @@ "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174", "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5" ], - "markers": "python_full_version >= '3.6.0'", + "markers": "python_version >= '3.6'", "version": "==6.0" }, "requests": { @@ -549,7 +665,7 @@ "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983", "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349" ], - "markers": "python_version >= '3.7' and python_version < '4'", + "markers": "python_full_version >= '3.7.0' and python_full_version < '4.0.0'", "version": "==2.28.1" }, "responses": { @@ -557,7 +673,7 @@ "sha256:396acb2a13d25297789a5866b4881cf4e46ffd49cc26c43ab1117f40b973102e", "sha256:dcf294d204d14c436fddcc74caefdbc5764795a40ff4e6a7740ed8ddbf3294be" ], - "markers": "python_version >= '3.7'", + "markers": "python_full_version >= '3.7.0'", "version": "==0.22.0" }, "ruamel.yaml": { @@ -612,7 +728,7 @@ "sha256:06176b74f3a15f61f1b4f25a1fc29a4429040b7647133a463da8fa5bd28d5ecd", "sha256:2ed07d3866f523cc561bf4a00fc5535827981b117dd7876f036b0c1aca42c947" ], - "markers": "python_version >= '3.7'", + "markers": "python_full_version >= '3.7.0'", "version": "==0.6.0" }, "safety": { @@ -625,11 +741,11 @@ }, "setuptools": { "hashes": [ - "sha256:512e5536220e38146176efb833d4a62aa726b7bbff82cfbc8ba9eaa3996e0b17", - "sha256:f62ea9da9ed6289bfe868cd6845968a2c854d1427f8548d52cae02a42b4f0356" + "sha256:d0b9a8433464d5800cbe05094acf5c6d52a91bfac9b52bcfc4d41382be5d5d31", + "sha256:e197a19aa8ec9722928f2206f8de752def0e4c9fc6953527360d1c36d94ddb2f" ], - "markers": "python_version >= '3.7'", - "version": "==65.5.0" + "markers": "python_full_version >= '3.7.0'", + "version": "==65.5.1" }, "shodan": { "hashes": [ @@ -651,7 +767,7 @@ "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94", "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936" ], - "markers": "python_full_version >= '3.6.0'", + "markers": "python_version >= '3.6'", "version": "==5.0.0" }, "stevedore": { @@ -697,7 +813,7 @@ "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa", "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e" ], - "markers": "python_version >= '3.7'", + "markers": "python_full_version >= '3.7.0'", "version": "==4.4.0" }, "urllib3": { @@ -705,7 +821,7 @@ "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e", "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' and python_version < '4'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' and python_full_version < '4.0.0'", "version": "==1.26.12" }, "vulture": { @@ -716,12 +832,20 @@ "index": "pypi", "version": "==2.6" }, + "websocket-client": { + "hashes": [ + "sha256:d6b06432f184438d99ac1f456eaf22fe1ade524c3dd16e661142dc54e9cba574", + "sha256:d6e8f90ca8e2dd4e8027c4561adeb9456b54044312dba655e7cae652ceb9ae59" + ], + "markers": "python_full_version >= '3.7.0'", + "version": "==1.4.2" + }, "werkzeug": { "hashes": [ "sha256:7ea2d48322cc7c0f8b3a215ed73eabd7b5d75d0b50e31ab006286ccff9e00b8f", "sha256:f979ab81f58d7318e064e99c4506445d60135ac5cd2e177a2de0089bfd4c9bd5" ], - "markers": "python_version >= '3.7'", + "markers": "python_full_version >= '3.7.0'", "version": "==2.2.2" }, "xlsxwriter": { @@ -739,6 +863,14 @@ ], "markers": "python_version >= '3.4'", "version": "==0.13.0" + }, + "zipp": { + "hashes": [ + "sha256:4fcb6f278987a6605757302a6e40e896257570d11c51628968ccb2a47e80c6c1", + "sha256:7a7262fd930bd3e36c50b9a64897aec3fafff3dfdeec9623ae22b40e93f99bb8" + ], + "markers": "python_version < '3.10'", + "version": "==3.10.0" } }, "develop": {} diff --git a/providers/aws/services/cloudformation/__init__.py b/providers/aws/services/cloudformation/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/providers/aws/services/cloudformation/check_extra7154 b/providers/aws/services/cloudformation/check_extra7154 deleted file mode 100644 index 7d4dcc42..00000000 --- a/providers/aws/services/cloudformation/check_extra7154 +++ /dev/null @@ -1,60 +0,0 @@ -#!/usr/bin/env bash - -# Prowler - the handy cloud security tool (copyright 2019) by Toni de la Fuente -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may not -# use this file except in compliance with the License. You may obtain a copy -# of the License at http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software distributed -# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR -# CONDITIONS OF ANY KIND, either express or implied. See the License for the -# specific language governing permissions and limitations under the License. -# -# Remediation: -# -# https://docs.aws.amazon.com/cli/latest/reference/cloudformation/update-termination-protection.html -# -# aws cloudformation update-termination-protection \ -# --stack-name my-stack \ -# --enable-termination-protection - -CHECK_ID_extra7154="7.154" -CHECK_TITLE_extra7154="[extra7154] Enable termination protection for Cloudformation Stacks" -CHECK_SCORED_extra7154="NOT_SCORED" -CHECK_CIS_LEVEL_extra7154="EXTRA" -CHECK_SEVERITY_extra7154="Medium" -CHECK_ASFF_RESOURCE_TYPE_extra7154="AwsCloudFormationStack" -CHECK_ALTERNATE_check7154="extra7154" -CHECK_SERVICENAME_extra7154="cloudformation" -CHECK_RISK_extra7154='Without termination protection enabled; a critical cloudformation stack can be accidently deleted.' -CHECK_REMEDIATION_extra7154='Ensure termination protection is enabled for the cloudformation stacks' -CHECK_DOC_extra7154='https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-protect-stacks.html' -CHECK_CAF_EPIC_extra7154='Infrastructure Protection' - -extra7154() { - for regx in $REGIONS; do - CFN_STACKS=$($AWSCLI cloudformation describe-stacks $PROFILE_OPT --region $regx --output json 2>&1) - if [[ $(echo "$CFN_STACKS" | grep -E 'AccessDenied|UnauthorizedOperation|AuthorizationError') ]]; then - textInfo "$regx: Access Denied trying to describe stacks" "$regx" - continue - fi - LIST_OF_CFN_STACKS=$(echo $CFN_STACKS | jq -r '.Stacks[].StackName') - if [[ $LIST_OF_CFN_STACKS ]];then - for stack in $LIST_OF_CFN_STACKS; do - CFN_STACK_DETAILS=$($AWSCLI cloudformation describe-stacks $PROFILE_OPT --region $regx --stack-name $stack --output json) - TERMINATION_ENABLED=$(echo $CFN_STACK_DETAILS | jq -r '.Stacks[].EnableTerminationProtection') - ROOT_ID=$(echo $CFN_STACK_DETAILS | jq -r '.Stacks[].RootId') - if [[ $ROOT_ID != null && $TERMINATION_ENABLED == "false" ]]; then - textInfo "$regx: $stack is a nested stack. Enable termination protection on the root stack $ROOT_ID" "$regx" "$stack" "$ROOT_ID" - elif [[ $TERMINATION_ENABLED == "true" ]]; then - textPass "$regx: Cloudformation stack $stack has termination protection enabled" "$regx" "$stack" - else - textFail "$regx: Cloudformation stack $stack has termination protection disabled" "$regx" "$stack" - fi - done - else - textInfo "$regx: No Cloudformation stacks found" "$regx" - fi - done -} diff --git a/providers/aws/services/cloudformation/check_extra742 b/providers/aws/services/cloudformation/check_extra742 deleted file mode 100644 index d2a923e7..00000000 --- a/providers/aws/services/cloudformation/check_extra742 +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env bash - -# Prowler - the handy cloud security tool (copyright 2019) by Toni de la Fuente -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may not -# use this file except in compliance with the License. You may obtain a copy -# of the License at http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software distributed -# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR -# CONDITIONS OF ANY KIND, either express or implied. See the License for the -# specific language governing permissions and limitations under the License. -CHECK_ID_extra742="7.42" -CHECK_TITLE_extra742="[extra742] Find secrets in CloudFormation outputs" -CHECK_SCORED_extra742="NOT_SCORED" -CHECK_CIS_LEVEL_extra742="EXTRA" -CHECK_SEVERITY_extra742="Critical" -CHECK_ASFF_RESOURCE_TYPE_extra742="AwsCloudFormationStack" -CHECK_ALTERNATE_check742="extra742" -CHECK_SERVICENAME_extra742="cloudformation" -CHECK_RISK_extra742='Secrets hardcoded into CloudFormation outputs can be used by malware and bad actors to gain lateral access to other services.' -CHECK_REMEDIATION_extra742='Implement automated detective control (e.g. using tools like Prowler ) to scan accounts for passwords and secrets. Use secrets manager service to store and retrieve passwords and secrets. ' -CHECK_DOC_extra742='https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-secretsmanager-secret-generatesecretstring.html' -CHECK_CAF_EPIC_extra742='IAM' - -extra742(){ - SECRETS_TEMP_FOLDER="$PROWLER_DIR/secrets-$ACCOUNT_NUM" - if [[ ! -d $SECRETS_TEMP_FOLDER ]]; then - # this folder is deleted once this check is finished - mkdir "${SECRETS_TEMP_FOLDER}" - fi - - for regx in $REGIONS; do - CHECK_DETECT_SECRETS_INSTALLATION=$(secretsDetector) - if [[ $? -eq 241 ]]; then - textInfo "$regx: python library detect-secrets not found. Make sure it is installed correctly." "$regx" - else - CFN_STACKS=$("${AWSCLI}" cloudformation describe-stacks $PROFILE_OPT --region "${regx}" --output json 2>&1) - if grep -q -E 'AccessDenied|UnauthorizedOperation|AuthorizationError' <<< "$CFN_STACKS" ; then - textInfo "$regx: Access Denied trying to describe stacks" "$regx" - continue - fi - LIST_OF_CFN_STACKS=$(jq -r '.Stacks[].StackName' <<< "${CFN_STACKS}") - if [[ $LIST_OF_CFN_STACKS ]];then - for stackName in $LIST_OF_CFN_STACKS; do - CFN_OUTPUTS_FILE="$SECRETS_TEMP_FOLDER/extra742-${stackName}-${regx}-outputs.txt" - # OutputKey and OutputValue are separated by a colon because secrets-detector needs a way to link both values - jq --arg stackName "$stackName" -r '.Stacks[] | select( .StackName == $stackName ) | .Outputs[]? | "\(.OutputKey):\(.OutputValue)"' <<< "${CFN_STACKS}" > "${CFN_OUTPUTS_FILE}" - if [ -s "${CFN_OUTPUTS_FILE}" ];then - FINDINGS=$(secretsDetector file "${CFN_OUTPUTS_FILE}") - if [[ $FINDINGS -eq 0 ]]; then - textPass "$regx: No secrets found in stack ${stackName} Outputs" "$regx" "${stackName}" - # Delete file if nothing interesting is there - rm -f "${CFN_OUTPUTS_FILE}" - else - textFail "$regx: Potential secret found in stack ${stackName} Outputs" "$regx" "${stackName}" - # Delete file to not leave trace, user must look at the CFN Stack - rm -f "${CFN_OUTPUTS_FILE}" - fi - else - textInfo "$regx: CloudFormation stack ${stackName} has no Outputs" "$regx" - fi - done - else - textInfo "$regx: No CloudFormation stacks found" "$regx" - fi - fi - done - - # Cleanup temporary folder - if [[ -d $SECRETS_TEMP_FOLDER ]] - then - rm -rf "${SECRETS_TEMP_FOLDER}" - fi -} diff --git a/providers/aws/services/cloudformation/cloudformation_client.py b/providers/aws/services/cloudformation/cloudformation_client.py new file mode 100644 index 00000000..7d6de63a --- /dev/null +++ b/providers/aws/services/cloudformation/cloudformation_client.py @@ -0,0 +1,4 @@ +from providers.aws.lib.audit_info.audit_info import current_audit_info +from providers.aws.services.cloudformation.cloudformation_service import CloudFormation + +cloudformation_client = CloudFormation(current_audit_info) diff --git a/providers/aws/services/cloudformation/cloudformation_outputs_find_secrets/__init__.py b/providers/aws/services/cloudformation/cloudformation_outputs_find_secrets/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/providers/aws/services/cloudformation/cloudformation_outputs_find_secrets/cloudformation_outputs_find_secrets.metadata.json b/providers/aws/services/cloudformation/cloudformation_outputs_find_secrets/cloudformation_outputs_find_secrets.metadata.json new file mode 100644 index 00000000..d40a52fc --- /dev/null +++ b/providers/aws/services/cloudformation/cloudformation_outputs_find_secrets/cloudformation_outputs_find_secrets.metadata.json @@ -0,0 +1,35 @@ +{ + "Provider": "aws", + "CheckID": "cloudformation_outputs_find_secrets", + "CheckTitle": "Find secrets in CloudFormation outputs", + "CheckType": [], + "ServiceName": "cloudformation", + "SubServiceName": "", + "ResourceIdTemplate": "arn:partition:cloudformation:region:account-id:stack/resource-id", + "Severity": "critical", + "ResourceType": "AwsCloudFormationStack", + "Description": "Find secrets in CloudFormation outputs", + "Risk": "Secrets hardcoded into CloudFormation outputs can be used by malware and bad actors to gain lateral access to other services.", + "RelatedUrl": "https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-secretsmanager-secret-generatesecretstring.html", + "Remediation": { + "Code": { + "CLI": "https://docs.bridgecrew.io/docs/bc_aws_secrets_2#cli-command", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Implement automated detective control to scan accounts for passwords and secrets. Use secrets manager service to store and retrieve passwords and secrets.", + "Url": "https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-secretsmanager-secret-generatesecretstring.html" + } + }, + "Categories": [], + "Tags": { + "Tag1Key": "value", + "Tag2Key": "value" + }, + "DependsOn": [], + "RelatedTo": [], + "Notes": "Infrastructure Protection", + "Compliance": [] +} diff --git a/providers/aws/services/cloudformation/cloudformation_outputs_find_secrets/cloudformation_outputs_find_secrets.py b/providers/aws/services/cloudformation/cloudformation_outputs_find_secrets/cloudformation_outputs_find_secrets.py new file mode 100644 index 00000000..f2d6331c --- /dev/null +++ b/providers/aws/services/cloudformation/cloudformation_outputs_find_secrets/cloudformation_outputs_find_secrets.py @@ -0,0 +1,57 @@ +import os +import tempfile + +from detect_secrets import SecretsCollection +from detect_secrets.settings import default_settings + +from lib.check.models import Check, Check_Report +from providers.aws.services.cloudformation.cloudformation_client import ( + cloudformation_client, +) + + +class cloudformation_outputs_find_secrets(Check): + """Check if a CloudFormation Stack has secrets in their Outputs""" + + def execute(self): + """Execute the cloudformation_outputs_find_secrets check""" + findings = [] + for stack in cloudformation_client.stacks: + report = Check_Report(self.metadata) + report.region = stack.region + report.resource_id = stack.name + report.resource_arn = stack.arn + + if stack.outputs: + temp_output_file = tempfile.NamedTemporaryFile(delete=False) + + # Store the CloudFormation Stack Outputs into a file + for output in stack.outputs: + temp_output_file.write(f"{output}".encode()) + temp_output_file.close() + + # Init detect_secrets + secrets = SecretsCollection() + # Scan file for secrets + with default_settings(): + secrets.scan_file(temp_output_file.name) + + if secrets.json(): + report.status = "FAIL" + report.status_extended = ( + f"Potential secret found in Stack {stack.name} Outputs." + ) + else: + report.status = "PASS" + report.status_extended = ( + f"No secrets found in Stack {stack.name} Outputs." + ) + + os.remove(temp_output_file.name) + else: + report.status = "PASS" + report.status_extended = f"CloudFormation {stack.name} has no Outputs." + + findings.append(report) + + return findings diff --git a/providers/aws/services/cloudformation/cloudformation_outputs_find_secrets/cloudformation_outputs_find_secrets_test.py b/providers/aws/services/cloudformation/cloudformation_outputs_find_secrets/cloudformation_outputs_find_secrets_test.py new file mode 100644 index 00000000..4c7a54c7 --- /dev/null +++ b/providers/aws/services/cloudformation/cloudformation_outputs_find_secrets/cloudformation_outputs_find_secrets_test.py @@ -0,0 +1,133 @@ +from unittest import mock + +from providers.aws.services.cloudformation.cloudformation_service import Stack + +# Mock Test Region +AWS_REGION = "eu-west-1" + + +class Test_cloudformation_outputs_find_secrets: + def test_no_stacks(self): + cloudformation_client = mock.MagicMock + cloudformation_client.stacks = [] + with mock.patch( + "providers.aws.services.cloudformation.cloudformation_service.CloudFormation", + new=cloudformation_client, + ): + # Test Check + from providers.aws.services.cloudformation.cloudformation_outputs_find_secrets.cloudformation_outputs_find_secrets import ( + cloudformation_outputs_find_secrets, + ) + + check = cloudformation_outputs_find_secrets() + result = check.execute() + + assert len(result) == 0 + + def test_stack_secret_in_outputs(self): + cloudformation_client = mock.MagicMock + stack_name = "Test-Stack" + cloudformation_client.stacks = [ + Stack( + arn="arn:aws:cloudformation:eu-west-1:123456789012:stack/Test-Stack/796c8d26-b390-41d7-a23c-0702c4e78b60", + name=stack_name, + outputs=["DB_PASSWORD:foobar123", "ENV:DEV"], + region=AWS_REGION, + ) + ] + + with mock.patch( + "providers.aws.services.cloudformation.cloudformation_service.CloudFormation", + cloudformation_client, + ): + from providers.aws.services.cloudformation.cloudformation_outputs_find_secrets.cloudformation_outputs_find_secrets import ( + cloudformation_outputs_find_secrets, + ) + + check = cloudformation_outputs_find_secrets() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Potential secret found in Stack {stack_name} Outputs." + ) + assert result[0].resource_id == "Test-Stack" + assert ( + result[0].resource_arn + == "arn:aws:cloudformation:eu-west-1:123456789012:stack/Test-Stack/796c8d26-b390-41d7-a23c-0702c4e78b60" + ) + assert result[0].region == AWS_REGION + + def test_stack_no_secret_in_outputs(self): + cloudformation_client = mock.MagicMock + stack_name = "Test-Stack" + cloudformation_client.stacks = [ + Stack( + arn="arn:aws:cloudformation:eu-west-1:123456789012:stack/Test-Stack/796c8d26-b390-41d7-a23c-0702c4e78b60", + name=stack_name, + outputs=["ENV:DEV"], + region=AWS_REGION, + ) + ] + + with mock.patch( + "providers.aws.services.cloudformation.cloudformation_service.CloudFormation", + cloudformation_client, + ): + from providers.aws.services.cloudformation.cloudformation_outputs_find_secrets.cloudformation_outputs_find_secrets import ( + cloudformation_outputs_find_secrets, + ) + + check = cloudformation_outputs_find_secrets() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"No secrets found in Stack {stack_name} Outputs." + ) + assert result[0].resource_id == "Test-Stack" + assert ( + result[0].resource_arn + == "arn:aws:cloudformation:eu-west-1:123456789012:stack/Test-Stack/796c8d26-b390-41d7-a23c-0702c4e78b60" + ) + assert result[0].region == AWS_REGION + + def test_stack_no_outputs(self): + cloudformation_client = mock.MagicMock + stack_name = "Test-Stack" + cloudformation_client.stacks = [ + Stack( + arn="arn:aws:cloudformation:eu-west-1:123456789012:stack/Test-Stack/796c8d26-b390-41d7-a23c-0702c4e78b60", + name=stack_name, + outputs=[], + region=AWS_REGION, + ) + ] + + with mock.patch( + "providers.aws.services.cloudformation.cloudformation_service.CloudFormation", + cloudformation_client, + ): + from providers.aws.services.cloudformation.cloudformation_outputs_find_secrets.cloudformation_outputs_find_secrets import ( + cloudformation_outputs_find_secrets, + ) + + check = cloudformation_outputs_find_secrets() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"CloudFormation {stack_name} has no Outputs." + ) + assert result[0].resource_id == "Test-Stack" + assert ( + result[0].resource_arn + == "arn:aws:cloudformation:eu-west-1:123456789012:stack/Test-Stack/796c8d26-b390-41d7-a23c-0702c4e78b60" + ) + assert result[0].region == AWS_REGION diff --git a/providers/aws/services/cloudformation/cloudformation_service.py b/providers/aws/services/cloudformation/cloudformation_service.py new file mode 100644 index 00000000..2584ac98 --- /dev/null +++ b/providers/aws/services/cloudformation/cloudformation_service.py @@ -0,0 +1,108 @@ +import threading +from dataclasses import dataclass + +from lib.logger import logger +from providers.aws.aws_provider import generate_regional_clients + + +################## CloudFormation +class CloudFormation: + def __init__(self, audit_info): + self.service = "cloudformation" + self.session = audit_info.audit_session + self.audited_account = audit_info.audited_account + self.regional_clients = generate_regional_clients(self.service, audit_info) + self.stacks = [] + self.__threading_call__(self.__describe_stacks__) + self.__describe_stack__() + + def __get_session__(self): + return self.session + + def __threading_call__(self, call): + threads = [] + for regional_client in self.regional_clients.values(): + threads.append(threading.Thread(target=call, args=(regional_client,))) + for t in threads: + t.start() + for t in threads: + t.join() + + def __describe_stacks__(self, regional_client): + """Get ALL CloudFormation Stacks""" + logger.info("CloudFormation - Describing Stacks...") + try: + describe_stacks_paginator = regional_client.get_paginator("describe_stacks") + for page in describe_stacks_paginator.paginate(): + for stack in page["Stacks"]: + outputs = [] + for output in stack["Outputs"]: + outputs.append(f"{output['OutputKey']}:{output['OutputValue']}") + self.stacks.append( + Stack( + arn=stack["StackId"], + name=stack["StackName"], + outputs=outputs, + region=regional_client.region, + ) + ) + except Exception as error: + logger.error( + f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + + def __describe_stack__(self): + """Get Details for a CloudFormation Stack""" + logger.info("CloudFormation - Describing Stack to get specific details...") + try: + for stack in self.stacks: + stack_details = self.regional_clients[stack.region].describe_stacks( + StackName=stack.name + ) + # Termination Protection + stack.enable_termination_protection = stack_details["Stacks"][0][ + "EnableTerminationProtection" + ] + # Nested Stack + if "RootId" in stack_details["Stacks"][0]: + stack.root_nested_stack = stack_details["Stacks"][0]["RootId"] + stack.is_nested_stack = True if stack.root_nested_stack != "" else False + + except Exception as error: + logger.error( + f"{stack.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + + +@dataclass +class Stack: + """Stack holds a CloudFormation Stack""" + + arn: str + """In the CloudFormation API the "Stacks[].StackId" is the ARN""" + name: str + """Stacks[].StackName""" + outputs: list[str] + """Stacks[].Outputs""" + enable_termination_protection: bool + """Stacks[].EnableTerminationProtection""" + root_nested_stack: str + """Stacks[].RootId""" + is_nested_stack: str + """True if the Stack is a Nested Stack""" + region: str + + def __init__( + self, + arn, + name, + outputs, + region, + ): + self.arn = arn + self.name = name + self.outputs = outputs + self.enable_termination_protection = False + self.is_nested_stack = False + self.root_nested_stack = "" + self.region = region diff --git a/providers/aws/services/cloudformation/cloudformation_service_test.py b/providers/aws/services/cloudformation/cloudformation_service_test.py new file mode 100644 index 00000000..289f1fc5 --- /dev/null +++ b/providers/aws/services/cloudformation/cloudformation_service_test.py @@ -0,0 +1,213 @@ +import datetime +import json +from unittest.mock import patch + +import boto3 +import botocore +from boto3 import session +from dateutil.tz import tzutc +from moto import mock_cloudformation +from moto.core import DEFAULT_ACCOUNT_ID + +from providers.aws.lib.audit_info.audit_info import AWS_Audit_Info +from providers.aws.services.cloudformation.cloudformation_service import CloudFormation + +# Mock Test Region +AWS_REGION = "eu-west-1" + +# Dummy CloudFormation Template +dummy_template = { + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "Stack 1", + "Resources": { + "EC2Instance1": { + "Type": "AWS::EC2::Instance", + "Properties": { + "ImageId": "EXAMPLE_AMI_ID", + "KeyName": "dummy", + "InstanceType": "t2.micro", + "Tags": [ + {"Key": "Description", "Value": "Test tag"}, + {"Key": "Name", "Value": "Name tag for tests"}, + ], + }, + } + }, +} + + +# Mocking Access Analyzer Calls +make_api_call = botocore.client.BaseClient._make_api_call + +# As you can see the operation_name has the list_analyzers snake_case form but +# we are using the ListAnalyzers form. +# Rationale -> https://github.com/boto/botocore/blob/develop/botocore/client.py#L810:L816 +# +# We have to mock every AWS API call using Boto3 +def mock_make_api_call(self, operation_name, kwarg): + if operation_name == "CreateStack": + return { + "StackId": "arn:aws:cloudformation:eu-west-1:123456789012:stack/Test-Stack/796c8d26-b390-41d7-a23c-0702c4e78b60" + } + if operation_name == "DescribeStacks": + print(f"ARGS: {kwarg}") + if "StackName" in kwarg: + return { + "Stacks": [ + { + "StackId": "arn:aws:cloudformation:eu-west-1:123456789012:stack/Test-Stack/796c8d26-b390-41d7-a23c-0702c4e78b60", + "StackName": "Test-Stack", + "Description": "Stack 1", + "Parameters": [], + "CreationTime": datetime.datetime( + 2022, 11, 7, 9, 33, 51, tzinfo=tzutc() + ), + "StackStatus": "CREATE_COMPLETE", + "DisableRollback": False, + "NotificationARNs": [], + "Outputs": [ + { + "OutputKey": "TestOutput1", + "OutputValue": "TestValue1", + "Description": "Test Output Description.", + } + ], + "RoleARN": "arn:aws:iam::123456789012:role/moto", + "EnableTerminationProtection": True, + "Tags": [ + {"Key": "Tag1", "Value": "Value1"}, + {"Key": "Tag2", "Value": "Value2"}, + ], + } + ] + } + # Return all Stacks + else: + return { + "Stacks": [ + { + "StackId": "arn:aws:cloudformation:eu-west-1:123456789012:stack/Test-Stack/796c8d26-b390-41d7-a23c-0702c4e78b60", + "StackName": "Test-Stack", + "Description": "Stack 1", + "Parameters": [], + "CreationTime": datetime.datetime( + 2022, 11, 7, 9, 33, 51, tzinfo=tzutc() + ), + "StackStatus": "CREATE_COMPLETE", + "DisableRollback": False, + "NotificationARNs": [], + "Outputs": [ + { + "OutputKey": "TestOutput1", + "OutputValue": "TestValue1", + "Description": "Test Output Description.", + } + ], + "RoleARN": "arn:aws:iam::123456789012:role/moto", + "Tags": [ + {"Key": "Tag1", "Value": "Value1"}, + {"Key": "Tag2", "Value": "Value2"}, + ], + } + ] + } + + return make_api_call(self, operation_name, kwarg) + + +# Mock generate_regional_clients() +def mock_generate_regional_clients(service, audit_info): + regional_client = audit_info.audit_session.client(service, region_name=AWS_REGION) + regional_client.region = AWS_REGION + return {AWS_REGION: regional_client} + + +# Mock generate_regional_clients() +def mock_generate_regional_clients(service, audit_info): + regional_client = audit_info.audit_session.client(service, region_name=AWS_REGION) + regional_client.region = AWS_REGION + return {AWS_REGION: regional_client} + + +# Patch every AWS call using Boto3 and generate_regional_clients to have 1 client +@patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call) +@patch( + "providers.aws.services.cloudformation.cloudformation_service.generate_regional_clients", + new=mock_generate_regional_clients, +) +class Test_CloudFormation_Service: + # Mocked Audit Info + def set_mocked_audit_info(self): + audit_info = AWS_Audit_Info( + original_session=None, + audit_session=session.Session( + profile_name=None, + botocore_session=None, + ), + audited_account=None, + audited_user_id=None, + audited_partition=None, + audited_identity_arn=None, + profile=None, + profile_region=None, + credentials=None, + assumed_role_info=None, + audited_regions=None, + organizations_metadata=None, + ) + return audit_info + + # Test CloudFormation Client + @mock_cloudformation + def test__get_client__(self): + cloudformation = CloudFormation(self.set_mocked_audit_info()) + assert ( + cloudformation.regional_clients[AWS_REGION].__class__.__name__ + == "CloudFormation" + ) + + # Test CloudFormation Service + @mock_cloudformation + def test__get_service__(self): + cloudformation = CloudFormation(self.set_mocked_audit_info()) + assert ( + cloudformation.regional_clients[AWS_REGION].__class__.__name__ + == "CloudFormation" + ) + + # Test CloudFormation Session + @mock_cloudformation + def test__get_session__(self): + cloudformation = CloudFormation(self.set_mocked_audit_info()) + assert cloudformation.session.__class__.__name__ == "Session" + + @mock_cloudformation + def test__describe_stacks__(self): + cloudformation_client = boto3.client("cloudformation", region_name=AWS_REGION) + stack_arn = cloudformation_client.create_stack( + StackName="Test-Stack", + TemplateBody=json.dumps(dummy_template), + RoleARN=f"arn:aws:iam::{DEFAULT_ACCOUNT_ID}:role/moto", + Tags=[ + {"Key": "Tag1", "Value": "Value1"}, + {"Key": "Tag2", "Value": "Value2"}, + ], + EnableTerminationProtection=True, + Outputs=[ + { + "OutputKey": "TestOutput1", + "OutputValue": "TestValue1", + "Description": "Test Output Description.", + } + ], + ) + + cloudformation = CloudFormation(self.set_mocked_audit_info()) + assert len(cloudformation.stacks) == 1 + assert cloudformation.stacks[0].arn == stack_arn["StackId"] + assert cloudformation.stacks[0].name == "Test-Stack" + assert cloudformation.stacks[0].outputs == ["TestOutput1:TestValue1"] + assert cloudformation.stacks[0].enable_termination_protection == True + assert cloudformation.stacks[0].is_nested_stack == False + assert cloudformation.stacks[0].root_nested_stack == "" + assert cloudformation.stacks[0].region == AWS_REGION diff --git a/providers/aws/services/cloudformation/cloudformation_stacks_termination_protection_enabled/__init__.py b/providers/aws/services/cloudformation/cloudformation_stacks_termination_protection_enabled/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/providers/aws/services/cloudformation/cloudformation_stacks_termination_protection_enabled/cloudformation_stacks_termination_protection_enabled.metadata.json b/providers/aws/services/cloudformation/cloudformation_stacks_termination_protection_enabled/cloudformation_stacks_termination_protection_enabled.metadata.json new file mode 100644 index 00000000..908cb1fe --- /dev/null +++ b/providers/aws/services/cloudformation/cloudformation_stacks_termination_protection_enabled/cloudformation_stacks_termination_protection_enabled.metadata.json @@ -0,0 +1,35 @@ +{ + "Provider": "aws", + "CheckID": "cloudformation_stacks_termination_protection_enabled", + "CheckTitle": "Enable termination protection for Cloudformation Stacks", + "CheckType": [], + "ServiceName": "cloudformation", + "SubServiceName": "", + "ResourceIdTemplate": "arn:partition:cloudformation:region:account-id:stack/resource-id", + "Severity": "medium", + "ResourceType": "AwsCloudFormationStack", + "Description": "Enable termination protection for Cloudformation Stacks", + "Risk": "Without termination protection enabled; a critical cloudformation stack can be accidently deleted.", + "RelatedUrl": "https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-protect-stacks.html", + "Remediation": { + "Code": { + "CLI": "aws cloudformation update-termination-protection --region us-east-1 --stack-name --enable-termination-protection", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Ensure termination protection is enabled for the cloudformation stacks.", + "Url": "https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-protect-stacks.html" + } + }, + "Categories": [], + "Tags": { + "Tag1Key": "value", + "Tag2Key": "value" + }, + "DependsOn": [], + "RelatedTo": [], + "Notes": "Infrastructure Protection", + "Compliance": [] +} diff --git a/providers/aws/services/cloudformation/cloudformation_stacks_termination_protection_enabled/cloudformation_stacks_termination_protection_enabled.py b/providers/aws/services/cloudformation/cloudformation_stacks_termination_protection_enabled/cloudformation_stacks_termination_protection_enabled.py new file mode 100644 index 00000000..c393ce95 --- /dev/null +++ b/providers/aws/services/cloudformation/cloudformation_stacks_termination_protection_enabled/cloudformation_stacks_termination_protection_enabled.py @@ -0,0 +1,28 @@ +from lib.check.models import Check, Check_Report +from providers.aws.services.cloudformation.cloudformation_client import ( + cloudformation_client, +) + + +class cloudformation_stacks_termination_protection_enabled(Check): + """Check if a CloudFormation Stack has the Termination Protection enabled""" + + def execute(self): + """Execute the cloudformation_stacks_termination_protection_enabled check""" + findings = [] + for stack in cloudformation_client.stacks: + if not stack.is_nested_stack: + report = Check_Report(self.metadata) + report.region = stack.region + report.resource_id = stack.name + report.resource_arn = stack.arn + + if stack.enable_termination_protection: + report.status = "PASS" + report.status_extended = f"CloudFormation {stack.name} has termination protection enabled" + else: + report.status = "FAIL" + report.status_extended = f"CloudFormation {stack.name} has termination protection disabled" + findings.append(report) + + return findings diff --git a/providers/aws/services/cloudformation/cloudformation_stacks_termination_protection_enabled/cloudformation_stacks_termination_protection_enabled_test.py b/providers/aws/services/cloudformation/cloudformation_stacks_termination_protection_enabled/cloudformation_stacks_termination_protection_enabled_test.py new file mode 100644 index 00000000..2307615b --- /dev/null +++ b/providers/aws/services/cloudformation/cloudformation_stacks_termination_protection_enabled/cloudformation_stacks_termination_protection_enabled_test.py @@ -0,0 +1,99 @@ +from unittest import mock + +from providers.aws.services.cloudformation.cloudformation_service import Stack + +# Mock Test Region +AWS_REGION = "eu-west-1" + + +class Test_cloudformation_stacks_termination_protection_enabled: + def test_no_stacks(self): + cloudformation_client = mock.MagicMock + cloudformation_client.stacks = [] + with mock.patch( + "providers.aws.services.cloudformation.cloudformation_service.CloudFormation", + new=cloudformation_client, + ): + # Test Check + from providers.aws.services.cloudformation.cloudformation_stacks_termination_protection_enabled.cloudformation_stacks_termination_protection_enabled import ( + cloudformation_stacks_termination_protection_enabled, + ) + + check = cloudformation_stacks_termination_protection_enabled() + result = check.execute() + + assert len(result) == 0 + + def test_stack_termination_protection_enabled(self): + cloudformation_client = mock.MagicMock + stack_name = "Test-Stack" + cloudformation_client.stacks = [ + Stack( + arn="arn:aws:cloudformation:eu-west-1:123456789012:stack/Test-Stack/796c8d26-b390-41d7-a23c-0702c4e78b60", + name=stack_name, + outputs="", + region=AWS_REGION, + ) + ] + cloudformation_client.stacks[0].enable_termination_protection = True + + with mock.patch( + "providers.aws.services.cloudformation.cloudformation_service.CloudFormation", + cloudformation_client, + ): + from providers.aws.services.cloudformation.cloudformation_stacks_termination_protection_enabled.cloudformation_stacks_termination_protection_enabled import ( + cloudformation_stacks_termination_protection_enabled, + ) + + check = cloudformation_stacks_termination_protection_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"CloudFormation {stack_name} has termination protection enabled" + ) + assert result[0].resource_id == "Test-Stack" + assert ( + result[0].resource_arn + == "arn:aws:cloudformation:eu-west-1:123456789012:stack/Test-Stack/796c8d26-b390-41d7-a23c-0702c4e78b60" + ) + assert result[0].region == AWS_REGION + + def test_stack_termination_protection_disabled(self): + cloudformation_client = mock.MagicMock + stack_name = "Test-Stack" + cloudformation_client.stacks = [ + Stack( + arn="arn:aws:cloudformation:eu-west-1:123456789012:stack/Test-Stack/796c8d26-b390-41d7-a23c-0702c4e78b60", + name=stack_name, + outputs="", + region=AWS_REGION, + ) + ] + cloudformation_client.stacks[0].enable_termination_protection = False + + with mock.patch( + "providers.aws.services.cloudformation.cloudformation_service.CloudFormation", + cloudformation_client, + ): + from providers.aws.services.cloudformation.cloudformation_stacks_termination_protection_enabled.cloudformation_stacks_termination_protection_enabled import ( + cloudformation_stacks_termination_protection_enabled, + ) + + check = cloudformation_stacks_termination_protection_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"CloudFormation {stack_name} has termination protection disabled" + ) + assert result[0].resource_id == "Test-Stack" + assert ( + result[0].resource_arn + == "arn:aws:cloudformation:eu-west-1:123456789012:stack/Test-Stack/796c8d26-b390-41d7-a23c-0702c4e78b60" + ) + assert result[0].region == AWS_REGION