From d2e29a337fd6b331343dfb0e0c9a5fe05befb673 Mon Sep 17 00:00:00 2001 From: pedrooot Date: Tue, 10 Jun 2025 16:47:22 +0200 Subject: [PATCH] feat(compliance): generate a pdf from a prowler scan --- poetry.lock | 141 ++++++++++++- pyproject.toml | 3 +- .../assets/img/prowler_logo.png | Bin 0 -> 21933 bytes .../compliance_report/compliance_generator.py | 196 ++++++++++++++++++ util/compliance_report/readme.md | 3 + 5 files changed, 338 insertions(+), 5 deletions(-) create mode 100644 util/compliance_report/assets/img/prowler_logo.png create mode 100644 util/compliance_report/compliance_generator.py create mode 100644 util/compliance_report/readme.md diff --git a/poetry.lock b/poetry.lock index bea5bae5ff..4003e954d4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. [[package]] name = "about-time" @@ -985,6 +985,18 @@ graph = ["pydot"] junit = ["junit-xml (>=1.9,<2.0)"] sarif = ["jschema_to_python (>=1.2.3,<1.3.0)", "sarif-om (>=1.0.4,<1.1.0)"] +[[package]] +name = "chardet" +version = "5.2.0" +description = "Universal encoding detector for Python 3" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970"}, + {file = "chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7"}, +] + [[package]] name = "charset-normalizer" version = "3.4.1" @@ -2184,8 +2196,6 @@ python-versions = "*" groups = ["dev"] files = [ {file = "jsonpath-ng-1.7.0.tar.gz", hash = "sha256:f6f5f7fd4e5ff79c785f1573b394043b39849fb2bb47bcead935d12b00beab3c"}, - {file = "jsonpath_ng-1.7.0-py2-none-any.whl", hash = "sha256:898c93fc173f0c336784a3fa63d7434297544b7198124a68f9a3ef9597b0ae6e"}, - {file = "jsonpath_ng-1.7.0-py3-none-any.whl", hash = "sha256:f3d7f9e848cba1b6da28c55b1c26ff915dc9e0b1ba7e752a53d6da8d5cbd00b6"}, ] [package.dependencies] @@ -3436,6 +3446,106 @@ files = [ [package.dependencies] setuptools = "*" +[[package]] +name = "pillow" +version = "11.2.1" +description = "Python Imaging Library (Fork)" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pillow-11.2.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:d57a75d53922fc20c165016a20d9c44f73305e67c351bbc60d1adaf662e74047"}, + {file = "pillow-11.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:127bf6ac4a5b58b3d32fc8289656f77f80567d65660bc46f72c0d77e6600cc95"}, + {file = "pillow-11.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4ba4be812c7a40280629e55ae0b14a0aafa150dd6451297562e1764808bbe61"}, + {file = "pillow-11.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8bd62331e5032bc396a93609982a9ab6b411c05078a52f5fe3cc59234a3abd1"}, + {file = "pillow-11.2.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:562d11134c97a62fe3af29581f083033179f7ff435f78392565a1ad2d1c2c45c"}, + {file = "pillow-11.2.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:c97209e85b5be259994eb5b69ff50c5d20cca0f458ef9abd835e262d9d88b39d"}, + {file = "pillow-11.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0c3e6d0f59171dfa2e25d7116217543310908dfa2770aa64b8f87605f8cacc97"}, + {file = "pillow-11.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc1c3bc53befb6096b84165956e886b1729634a799e9d6329a0c512ab651e579"}, + {file = "pillow-11.2.1-cp310-cp310-win32.whl", hash = "sha256:312c77b7f07ab2139924d2639860e084ec2a13e72af54d4f08ac843a5fc9c79d"}, + {file = "pillow-11.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:9bc7ae48b8057a611e5fe9f853baa88093b9a76303937449397899385da06fad"}, + {file = "pillow-11.2.1-cp310-cp310-win_arm64.whl", hash = "sha256:2728567e249cdd939f6cc3d1f049595c66e4187f3c34078cbc0a7d21c47482d2"}, + {file = "pillow-11.2.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:35ca289f712ccfc699508c4658a1d14652e8033e9b69839edf83cbdd0ba39e70"}, + {file = "pillow-11.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0409af9f829f87a2dfb7e259f78f317a5351f2045158be321fd135973fff7bf"}, + {file = "pillow-11.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4e5c5edee874dce4f653dbe59db7c73a600119fbea8d31f53423586ee2aafd7"}, + {file = "pillow-11.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b93a07e76d13bff9444f1a029e0af2964e654bfc2e2c2d46bfd080df5ad5f3d8"}, + {file = "pillow-11.2.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:e6def7eed9e7fa90fde255afaf08060dc4b343bbe524a8f69bdd2a2f0018f600"}, + {file = "pillow-11.2.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8f4f3724c068be008c08257207210c138d5f3731af6c155a81c2b09a9eb3a788"}, + {file = "pillow-11.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a0a6709b47019dff32e678bc12c63008311b82b9327613f534e496dacaefb71e"}, + {file = "pillow-11.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f6b0c664ccb879109ee3ca702a9272d877f4fcd21e5eb63c26422fd6e415365e"}, + {file = "pillow-11.2.1-cp311-cp311-win32.whl", hash = "sha256:cc5d875d56e49f112b6def6813c4e3d3036d269c008bf8aef72cd08d20ca6df6"}, + {file = "pillow-11.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:0f5c7eda47bf8e3c8a283762cab94e496ba977a420868cb819159980b6709193"}, + {file = "pillow-11.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:4d375eb838755f2528ac8cbc926c3e31cc49ca4ad0cf79cff48b20e30634a4a7"}, + {file = "pillow-11.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:78afba22027b4accef10dbd5eed84425930ba41b3ea0a86fa8d20baaf19d807f"}, + {file = "pillow-11.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78092232a4ab376a35d68c4e6d5e00dfd73454bd12b230420025fbe178ee3b0b"}, + {file = "pillow-11.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25a5f306095c6780c52e6bbb6109624b95c5b18e40aab1c3041da3e9e0cd3e2d"}, + {file = "pillow-11.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c7b29dbd4281923a2bfe562acb734cee96bbb129e96e6972d315ed9f232bef4"}, + {file = "pillow-11.2.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3e645b020f3209a0181a418bffe7b4a93171eef6c4ef6cc20980b30bebf17b7d"}, + {file = "pillow-11.2.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b2dbea1012ccb784a65349f57bbc93730b96e85b42e9bf7b01ef40443db720b4"}, + {file = "pillow-11.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:da3104c57bbd72948d75f6a9389e6727d2ab6333c3617f0a89d72d4940aa0443"}, + {file = "pillow-11.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:598174aef4589af795f66f9caab87ba4ff860ce08cd5bb447c6fc553ffee603c"}, + {file = "pillow-11.2.1-cp312-cp312-win32.whl", hash = "sha256:1d535df14716e7f8776b9e7fee118576d65572b4aad3ed639be9e4fa88a1cad3"}, + {file = "pillow-11.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:14e33b28bf17c7a38eede290f77db7c664e4eb01f7869e37fa98a5aa95978941"}, + {file = "pillow-11.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:21e1470ac9e5739ff880c211fc3af01e3ae505859392bf65458c224d0bf283eb"}, + {file = "pillow-11.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fdec757fea0b793056419bca3e9932eb2b0ceec90ef4813ea4c1e072c389eb28"}, + {file = "pillow-11.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0e130705d568e2f43a17bcbe74d90958e8a16263868a12c3e0d9c8162690830"}, + {file = "pillow-11.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bdb5e09068332578214cadd9c05e3d64d99e0e87591be22a324bdbc18925be0"}, + {file = "pillow-11.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d189ba1bebfbc0c0e529159631ec72bb9e9bc041f01ec6d3233d6d82eb823bc1"}, + {file = "pillow-11.2.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:191955c55d8a712fab8934a42bfefbf99dd0b5875078240943f913bb66d46d9f"}, + {file = "pillow-11.2.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:ad275964d52e2243430472fc5d2c2334b4fc3ff9c16cb0a19254e25efa03a155"}, + {file = "pillow-11.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:750f96efe0597382660d8b53e90dd1dd44568a8edb51cb7f9d5d918b80d4de14"}, + {file = "pillow-11.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fe15238d3798788d00716637b3d4e7bb6bde18b26e5d08335a96e88564a36b6b"}, + {file = "pillow-11.2.1-cp313-cp313-win32.whl", hash = "sha256:3fe735ced9a607fee4f481423a9c36701a39719252a9bb251679635f99d0f7d2"}, + {file = "pillow-11.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:74ee3d7ecb3f3c05459ba95eed5efa28d6092d751ce9bf20e3e253a4e497e691"}, + {file = "pillow-11.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:5119225c622403afb4b44bad4c1ca6c1f98eed79db8d3bc6e4e160fc6339d66c"}, + {file = "pillow-11.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8ce2e8411c7aaef53e6bb29fe98f28cd4fbd9a1d9be2eeea434331aac0536b22"}, + {file = "pillow-11.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9ee66787e095127116d91dea2143db65c7bb1e232f617aa5957c0d9d2a3f23a7"}, + {file = "pillow-11.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9622e3b6c1d8b551b6e6f21873bdcc55762b4b2126633014cea1803368a9aa16"}, + {file = "pillow-11.2.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63b5dff3a68f371ea06025a1a6966c9a1e1ee452fc8020c2cd0ea41b83e9037b"}, + {file = "pillow-11.2.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:31df6e2d3d8fc99f993fd253e97fae451a8db2e7207acf97859732273e108406"}, + {file = "pillow-11.2.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:062b7a42d672c45a70fa1f8b43d1d38ff76b63421cbbe7f88146b39e8a558d91"}, + {file = "pillow-11.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4eb92eca2711ef8be42fd3f67533765d9fd043b8c80db204f16c8ea62ee1a751"}, + {file = "pillow-11.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f91ebf30830a48c825590aede79376cb40f110b387c17ee9bd59932c961044f9"}, + {file = "pillow-11.2.1-cp313-cp313t-win32.whl", hash = "sha256:e0b55f27f584ed623221cfe995c912c61606be8513bfa0e07d2c674b4516d9dd"}, + {file = "pillow-11.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:36d6b82164c39ce5482f649b437382c0fb2395eabc1e2b1702a6deb8ad647d6e"}, + {file = "pillow-11.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:225c832a13326e34f212d2072982bb1adb210e0cc0b153e688743018c94a2681"}, + {file = "pillow-11.2.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:7491cf8a79b8eb867d419648fff2f83cb0b3891c8b36da92cc7f1931d46108c8"}, + {file = "pillow-11.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8b02d8f9cb83c52578a0b4beadba92e37d83a4ef11570a8688bbf43f4ca50909"}, + {file = "pillow-11.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:014ca0050c85003620526b0ac1ac53f56fc93af128f7546623cc8e31875ab928"}, + {file = "pillow-11.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3692b68c87096ac6308296d96354eddd25f98740c9d2ab54e1549d6c8aea9d79"}, + {file = "pillow-11.2.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:f781dcb0bc9929adc77bad571b8621ecb1e4cdef86e940fe2e5b5ee24fd33b35"}, + {file = "pillow-11.2.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:2b490402c96f907a166615e9a5afacf2519e28295f157ec3a2bb9bd57de638cb"}, + {file = "pillow-11.2.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dd6b20b93b3ccc9c1b597999209e4bc5cf2853f9ee66e3fc9a400a78733ffc9a"}, + {file = "pillow-11.2.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:4b835d89c08a6c2ee7781b8dd0a30209a8012b5f09c0a665b65b0eb3560b6f36"}, + {file = "pillow-11.2.1-cp39-cp39-win32.whl", hash = "sha256:b10428b3416d4f9c61f94b494681280be7686bda15898a3a9e08eb66a6d92d67"}, + {file = "pillow-11.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:6ebce70c3f486acf7591a3d73431fa504a4e18a9b97ff27f5f47b7368e4b9dd1"}, + {file = "pillow-11.2.1-cp39-cp39-win_arm64.whl", hash = "sha256:c27476257b2fdcd7872d54cfd119b3a9ce4610fb85c8e32b70b42e3680a29a1e"}, + {file = "pillow-11.2.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:9b7b0d4fd2635f54ad82785d56bc0d94f147096493a79985d0ab57aedd563156"}, + {file = "pillow-11.2.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:aa442755e31c64037aa7c1cb186e0b369f8416c567381852c63444dd666fb772"}, + {file = "pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0d3348c95b766f54b76116d53d4cb171b52992a1027e7ca50c81b43b9d9e363"}, + {file = "pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85d27ea4c889342f7e35f6d56e7e1cb345632ad592e8c51b693d7b7556043ce0"}, + {file = "pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bf2c33d6791c598142f00c9c4c7d47f6476731c31081331664eb26d6ab583e01"}, + {file = "pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e616e7154c37669fc1dfc14584f11e284e05d1c650e1c0f972f281c4ccc53193"}, + {file = "pillow-11.2.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:39ad2e0f424394e3aebc40168845fee52df1394a4673a6ee512d840d14ab3013"}, + {file = "pillow-11.2.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:80f1df8dbe9572b4b7abdfa17eb5d78dd620b1d55d9e25f834efdbee872d3aed"}, + {file = "pillow-11.2.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ea926cfbc3957090becbcbbb65ad177161a2ff2ad578b5a6ec9bb1e1cd78753c"}, + {file = "pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:738db0e0941ca0376804d4de6a782c005245264edaa253ffce24e5a15cbdc7bd"}, + {file = "pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db98ab6565c69082ec9b0d4e40dd9f6181dab0dd236d26f7a50b8b9bfbd5076"}, + {file = "pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:036e53f4170e270ddb8797d4c590e6dd14d28e15c7da375c18978045f7e6c37b"}, + {file = "pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:14f73f7c291279bd65fda51ee87affd7c1e097709f7fdd0188957a16c264601f"}, + {file = "pillow-11.2.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:208653868d5c9ecc2b327f9b9ef34e0e42a4cdd172c2988fd81d62d2bc9bc044"}, + {file = "pillow-11.2.1.tar.gz", hash = "sha256:a64dd61998416367b7ef979b73d3a85853ba9bec4c2925f74e588879a58716b6"}, +] + +[package.extras] +docs = ["furo", "olefile", "sphinx (>=8.2)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] +fpx = ["olefile"] +mic = ["olefile"] +test-arrow = ["pyarrow"] +tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout", "trove-classifiers (>=2024.10.12)"] +typing = ["typing-extensions ; python_version < \"3.10\""] +xmp = ["defusedxml"] + [[package]] name = "platformdirs" version = "4.3.7" @@ -4337,6 +4447,29 @@ files = [ {file = "regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519"}, ] +[[package]] +name = "reportlab" +version = "4.4.1" +description = "The Reportlab Toolkit" +optional = false +python-versions = "<4,>=3.7" +groups = ["main"] +files = [ + {file = "reportlab-4.4.1-py3-none-any.whl", hash = "sha256:9217a1c8c1917217f819718b24972a96ad0c485a1c494749562d097b58d974b7"}, + {file = "reportlab-4.4.1.tar.gz", hash = "sha256:5f9b9fc0b7a48e8912c25ccf69d26b82980ab0da718e4f583fa720e8f8f5073f"}, +] + +[package.dependencies] +chardet = "*" +pillow = ">=9.0.0" + +[package.extras] +accel = ["rl_accel (>=0.9.0,<1.1)"] +bidi = ["rlbidi"] +pycairo = ["freetype-py (>=2.3.0,<2.4)", "rlPyCairo (>=0.2.0,<1)"] +renderpm = ["rl_renderPM (>=4.0.3,<4.1)"] +shaping = ["uharfbuzz"] + [[package]] name = "requests" version = "2.32.3" @@ -5456,4 +5589,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = ">3.9.1,<3.13" -content-hash = "f504af1d00a1da9dd65269509daf32f919c13be6c46f25cd9ef9bfba6b9c9a07" +content-hash = "c2c48285391fc3a3f721c96211b6c9b0eff54f1b948a9d3bef5749d4d3298c41" diff --git a/pyproject.toml b/pyproject.toml index 26cc8d967e..0c0cb57661 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,7 +57,8 @@ dependencies = [ "shodan==1.31.0", "slack-sdk==3.34.0", "tabulate==0.9.0", - "tzlocal==5.3.1" + "tzlocal==5.3.1", + "reportlab (>=4.4.1,<5.0.0)" ] description = "Prowler is an Open Source security tool to perform AWS, GCP and Azure security best practices assessments, audits, incident response, continuous monitoring, hardening and forensics readiness. It contains hundreds of controls covering CIS, NIST 800, NIST CSF, CISA, RBI, FedRAMP, PCI-DSS, GDPR, HIPAA, FFIEC, SOC2, GXP, AWS Well-Architected Framework Security Pillar, AWS Foundational Technical Review (FTR), ENS (Spanish National Security Scheme) and your custom security frameworks." license = "Apache-2.0" diff --git a/util/compliance_report/assets/img/prowler_logo.png b/util/compliance_report/assets/img/prowler_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..3a6ed6490d6ead3f578cd658e5030c8f3e17282b GIT binary patch literal 21933 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 literal 0 HcmV?d00001 diff --git a/util/compliance_report/compliance_generator.py b/util/compliance_report/compliance_generator.py new file mode 100644 index 0000000000..0becdba654 --- /dev/null +++ b/util/compliance_report/compliance_generator.py @@ -0,0 +1,196 @@ +import requests +from reportlab.lib.pagesizes import letter +from reportlab.lib.styles import getSampleStyleSheet +from reportlab.lib.units import inch +from reportlab.platypus import Image, PageBreak, Paragraph, SimpleDocTemplate, Spacer + + +def generate_compliance_report( + scan_id: str, compliance_id: str, output_path: str, email: str, password: str +): + """ + Generate a PDF compliance report based on Prowler endpoints. + + Parameters: + - scan_id: ID of the scan executed by Prowler. + - compliance_id: ID of the compliance framework (e.g., "nis2_azure"). + - output_path: Output PDF file path (e.g., "compliance_report.pdf"). + - email: Email for the API authentication. + - password: Password for the API. + """ + styles = getSampleStyleSheet() + title_style = styles["Title"] + h1 = styles["Heading1"] + h2 = styles["Heading2"] + h3 = styles["Heading3"] + normal = styles["Normal"] + + # Call to this endpoint to get the credentials + url_credentials = "http://localhost:8080/api/v1/tokens" + payload = { + "data": { + "type": "tokens", + "attributes": { + "email": email, + "password": password, + }, + } + } + resp_credentials = requests.post( + url_credentials, + json=payload, + headers={"Content-Type": "application/vnd.api+json"}, + ).json() + token = resp_credentials.get("data", {}).get("attributes", {}).get("access") + + url_reqs = f"http://localhost:8080/api/v1/compliance-overviews/requirements?filter[compliance_id]={compliance_id}&filter[scan_id]={scan_id}" + resp_reqs = ( + requests.get(url_reqs, headers={"Authorization": f"Bearer {token}"}) + .json() + .get("data", []) + ) + + url_attrs = f"http://localhost:8080/api/v1/compliance-overviews/attributes?filter[compliance_id]={compliance_id}" + resp_attrs = ( + requests.get(url_attrs, headers={"Authorization": f"Bearer {token}"}) + .json() + .get("data", []) + ) + + compliance_name = resp_reqs[0]["attributes"]["framework"] + compliance_version = resp_reqs[0]["attributes"]["version"] + + attrs_map = {item["id"]: item["attributes"] for item in resp_attrs} + + def get_finding_info(check_id: str): + url_find = f"http://localhost:8080/api/v1/findings?filter[check_id]={check_id}&filter[scan_id]={scan_id}" + value = ( + requests.get(url_find, headers={"Authorization": f"Bearer {token}"}) + .json() + .get("data", []) + ) + return value + + doc = SimpleDocTemplate(output_path, pagesize=letter) + elements = [] + + try: + logo = Image( + "util/compliance_report/assets/img/prowler_logo.png", + width=5 * inch, + height=1 * inch, + ) + elements.append(logo) + except Exception: + pass + elements.append(Spacer(1, 0.5 * inch)) + elements.append(Paragraph("Compliance Report - Prowler", title_style)) + elements.append(Spacer(1, 0.2 * inch)) + elements.append(Paragraph(f"Compliance ID: {compliance_id}", normal)) + elements.append(Paragraph(f"Scan ID: {scan_id}", normal)) + elements.append(Paragraph(f"Compliance Name: {compliance_name}", normal)) + elements.append( + Paragraph(f"Compliance Version: {compliance_version}", normal) + ) + elements.append(PageBreak()) + + elements.append(Paragraph("Requirements Index", h1)) + + # Organize requirements by section and subsection + sections = {} + for req in resp_attrs: + meta = req["attributes"]["attributes"]["metadata"][0] + section = meta["Section"] + subsection = meta["SubSection"] + req_id = req["id"] + title = meta["Title"] + + if section not in sections: + sections[section] = {} + if subsection not in sections[section]: + sections[section][subsection] = [] + + sections[section][subsection].append({"id": req_id, "title": title}) + + # Generate hierarchical index + section_num = 1 + for section_name, subsections in sections.items(): + elements.append(Paragraph(f"{section_num}. {section_name}", h2)) + + subsection_num = 1 + for subsection_name, requirements in subsections.items(): + elements.append(Paragraph(f"{subsection_name}", h3)) + + req_num = 1 + for req in requirements: + elements.append(Paragraph(f"{req['id']} - {req['title']}", normal)) + req_num += 1 + + subsection_num += 1 + + section_num += 1 + elements.append(Spacer(1, 0.1 * inch)) + + elements.append(PageBreak()) + + for req in resp_reqs: + req_id = req["id"] + attr = attrs_map.get(req_id, {}) + desc = req["attributes"]["description"] + status = req["attributes"]["status"] + + elements.append(Paragraph(f"{req_id}: {attr.get('description', desc)}", h1)) + elements.append(Paragraph(f"State: {status}", normal)) + elements.append(Spacer(1, 0.1 * inch)) + + metadata = attr.get("attributes", {}).get("metadata", []) + if metadata: + m = metadata[0] + elements.append( + Paragraph(f"Descripción: {m.get('AttributeDescription')}", normal) + ) + elements.append( + Paragraph(f"Nivel de Riesgo: {m.get('LevelOfRisk')}", normal) + ) + elements.append(Paragraph(f"Peso: {m.get('Weight')}", normal)) + elements.append(Spacer(1, 0.1 * inch)) + + checks = attr.get("attributes", {}).get("check_ids", []) + for cid in checks: + elements.append(Paragraph(f"Check: {cid}", h2)) + finds = get_finding_info(cid) + if not finds: + elements.append(Paragraph("- Sin findings para este check.", normal)) + else: + for f in finds: + fid = f.get("id") + resource = f.get("attributes", {}).get("resource_id", "") + message = f.get("attributes", {}).get("message", "") + elements.append( + Paragraph(f"- [{fid}] {resource}: {message}", normal) + ) + elements.append(Spacer(1, 0.1 * inch)) + + elements.append(PageBreak()) + + doc.build(elements) + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser( + description="Generate a PDF compliance report with Prowler" + ) + parser.add_argument("--scan-id", required=True, help="Scan ID") + parser.add_argument("--compliance-id", required=True, help="Compliance ID") + parser.add_argument( + "--output", default="compliance_report.pdf", help="Output PDF file path" + ) + parser.add_argument("--email", required=True, help="Email for the API") + parser.add_argument("--password", required=True, help="Password for the API") + args = parser.parse_args() + + generate_compliance_report( + args.scan_id, args.compliance_id, args.output, args.email, args.password + ) diff --git a/util/compliance_report/readme.md b/util/compliance_report/readme.md new file mode 100644 index 0000000000..c55168fb72 --- /dev/null +++ b/util/compliance_report/readme.md @@ -0,0 +1,3 @@ +# How to use it? + +python3 util/compliance_report/compliance_generator.py --scan-id 01975963-8db9-7d65-8422-aded62a0a7ad --compliance-id prowler_threatscore_azure --email --password