mirror of
https://github.com/ghndrx/prowler.git
synced 2026-02-11 07:15:15 +00:00
feat(cloudformation): Service and Checks (#1454)
Co-authored-by: Sergio Garcia <38561120+sergargar@users.noreply.github.com>
This commit is contained in:
2
Pipfile
2
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"
|
||||
|
||||
202
Pipfile.lock
generated
202
Pipfile.lock
generated
@@ -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": {}
|
||||
|
||||
0
providers/aws/services/cloudformation/__init__.py
Normal file
0
providers/aws/services/cloudformation/__init__.py
Normal file
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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": []
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
108
providers/aws/services/cloudformation/cloudformation_service.py
Normal file
108
providers/aws/services/cloudformation/cloudformation_service.py
Normal file
@@ -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
|
||||
@@ -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
|
||||
@@ -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 <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": []
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user