refactor(iac): import checkov python library (#8093)

This commit is contained in:
Sergio Garcia
2025-06-25 21:36:21 +08:00
committed by GitHub
parent 80fd5d1ba6
commit 98da3059b4
12 changed files with 918 additions and 345 deletions
Generated
+5 -117
View File
@@ -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"
@@ -844,28 +844,6 @@ test = ["beautifulsoup4 (>=4.8.0)", "coverage (>=4.5.4)", "fixtures (>=3.0.0)",
toml = ["tomli (>=1.1.0) ; python_version < \"3.11\""]
yaml = ["PyYAML"]
[[package]]
name = "bc-detect-secrets"
version = "1.5.41"
description = "Tool for detecting secrets in the codebase"
optional = false
python-versions = ">=3.8"
groups = ["main"]
markers = "python_version >= \"3.10\""
files = [
{file = "bc_detect_secrets-1.5.41-py3-none-any.whl", hash = "sha256:629df912f2a4f4d5039cc1fece906c34700586f7db1ae6a8d1c830c25df6db9b"},
{file = "bc_detect_secrets-1.5.41.tar.gz", hash = "sha256:4bd08292a975bfc9b95771e118dd1131e1afbd479610eb29e4e0c15bd33677fc"},
]
[package.dependencies]
pyyaml = "*"
requests = "*"
unidiff = "*"
[package.extras]
gibberish = ["gibberish-detector"]
word-list = ["pyahocorasick"]
[[package]]
name = "bc-detect-secrets"
version = "1.5.44"
@@ -873,7 +851,6 @@ description = "Tool for detecting secrets in the codebase"
optional = false
python-versions = ">=3.8"
groups = ["main"]
markers = "python_version < \"3.10\""
files = [
{file = "bc_detect_secrets-1.5.44-py3-none-any.whl", hash = "sha256:0ab63d6c4f6680ec2dbe42cc3c63480568c55dbb6254afcc5bb6d4375a4e1d27"},
{file = "bc_detect_secrets-1.5.44.tar.gz", hash = "sha256:bebd82c56055c600335f85db95f7ca3b434087f16292a0396a60705de1b94183"},
@@ -1341,76 +1318,14 @@ files = [
[[package]]
name = "checkov"
version = "3.2.439"
version = "3.2.445"
description = "Infrastructure as code static analysis"
optional = false
python-versions = ">=3.8"
groups = ["main"]
markers = "python_version >= \"3.10\""
files = [
{file = "checkov-3.2.439-py3-none-any.whl", hash = "sha256:ab3a6b58989c979a5a6c337282a33f372fc1158c73215e3f488adffb56133f6f"},
{file = "checkov-3.2.439.tar.gz", hash = "sha256:31b0c1ea0cc99141cf333a20f6fff42d82130d5eb2ba12ef4e4e3b92c7bce68e"},
]
[package.dependencies]
aiodns = ">=3.0.0,<4.0.0"
aiohttp = ">=3.8.0,<4.0.0"
aiomultiprocess = ">=0.9.0,<0.10.0"
argcomplete = ">=3.0.0,<4.0.0"
asteval = "1.0.5"
bc-detect-secrets = "1.5.41"
bc-jsonpath-ng = "1.6.1"
bc-python-hcl2 = "0.4.2"
boto3 = "1.35.49"
cachetools = ">=5.2.0,<6.0.0"
charset-normalizer = ">=3.1.0,<4.0.0"
click = ">=8.1.0,<9.0.0"
cloudsplaining = ">=0.7.0,<0.8.0"
colorama = ">=0.4.3,<0.5.0"
configargparse = ">=1.5.3,<2.0.0"
cyclonedx-python-lib = ">=6.0.0,<8.0.0"
docker = ">=6.0.1,<8.0.0"
dockerfile-parse = ">=2.0.0,<3.0.0"
dpath = "2.1.3"
gitpython = ">=3.1.30,<4.0.0"
importlib-metadata = ">=6.0.0,<8.0.0"
jmespath = ">=1.0.0,<2.0.0"
jsonschema = ">=4.17.0,<5.0.0"
junit-xml = ">=1.9,<2.0"
license-expression = ">=30.1.0,<31.0.0"
networkx = "<2.7"
packageurl-python = ">=0.11.1,<0.14.0"
packaging = ">=23.0,<24.0"
prettytable = ">=3.6.0,<4.0.0"
pycep-parser = "0.5.1"
pydantic = ">=2.0.0,<3.0.0"
pyston = {version = "2.3.5", markers = "python_version < \"3.11\" and (sys_platform == \"linux\" or sys_platform == \"darwin\") and platform_machine == \"x86_64\" and implementation_name == \"cpython\""}
pyston-autoload = {version = "2.3.5", markers = "python_version < \"3.11\" and (sys_platform == \"linux\" or sys_platform == \"darwin\") and platform_machine == \"x86_64\" and implementation_name == \"cpython\""}
pyyaml = ">=6.0.0,<7.0.0"
requests = ">=2.28.0,<3.0.0"
rustworkx = ">=0.13.0,<1.0.0"
schema = "<=0.7.5"
spdx-tools = ">=0.8.0,<0.9.0"
tabulate = ">=0.9.0,<0.10.0"
termcolor = ">=1.1.0,<2.4.0"
tqdm = ">=4.65.0,<5.0.0"
typing-extensions = ">=4.5.0,<5.0.0"
yarl = ">=1.9.1,<2.0.0"
[package.extras]
dev = ["GitPython (==3.1.41)", "bandit", "coverage (==7.6.1)", "coverage-badge", "jsonschema", "pytest (<8.0.0)"]
[[package]]
name = "checkov"
version = "3.2.442"
description = "Infrastructure as code static analysis"
optional = false
python-versions = ">=3.8"
groups = ["main"]
markers = "python_version < \"3.10\""
files = [
{file = "checkov-3.2.442-py3-none-any.whl", hash = "sha256:e94a3283bff9b4a81e54e57b4a00b02259dec0d85b17bf17e00652d137bd1a6d"},
{file = "checkov-3.2.442.tar.gz", hash = "sha256:e5206872de63d389cfb1b7c1212ce4b5a147986152e890461d87251726b4b0e7"},
{file = "checkov-3.2.445-py3-none-any.whl", hash = "sha256:c0cd1109cbcf1c764198a7c64b911845b83776587a65e0677880ed67a39d1f73"},
{file = "checkov-3.2.445.tar.gz", hash = "sha256:78705d34a9c7234bd7076c1970daedd18e823f79e7d83d2998bb84695cc33ebc"},
]
[package.dependencies]
@@ -2738,8 +2653,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]
@@ -5673,7 +5586,6 @@ files = [
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f66efbc1caa63c088dead1c4170d148eabc9b80d95fb75b6c92ac0aad2437d76"},
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:22353049ba4181685023b25b5b51a574bce33e7f51c759371a7422dcae5402a6"},
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:932205970b9f9991b34f55136be327501903f7c66830e9760a8ffb15b07f05cd"},
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a52d48f4e7bf9005e8f0a89209bf9a73f7190ddf0489eee5eb51377385f59f2a"},
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win32.whl", hash = "sha256:3eac5a91891ceb88138c113f9db04f3cebdae277f5d44eaa3651a4f573e6a5da"},
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win_amd64.whl", hash = "sha256:ab007f2f5a87bd08ab1499bdf96f3d5c6ad4dcfa364884cb4549aa0154b13a28"},
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:4a6679521a58256a90b0d89e03992c15144c5f3858f40d7c18886023d7943db6"},
@@ -5682,7 +5594,6 @@ files = [
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:811ea1594b8a0fb466172c384267a4e5e367298af6b228931f273b111f17ef52"},
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cf12567a7b565cbf65d438dec6cfbe2917d3c1bdddfce84a9930b7d35ea59642"},
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7dd5adc8b930b12c8fc5b99e2d535a09889941aa0d0bd06f4749e9a9397c71d2"},
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1492a6051dab8d912fc2adeef0e8c72216b24d57bd896ea607cb90bb0c4981d3"},
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win32.whl", hash = "sha256:bd0a08f0bab19093c54e18a14a10b4322e1eacc5217056f3c063bd2f59853ce4"},
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win_amd64.whl", hash = "sha256:a274fb2cb086c7a3dea4322ec27f4cb5cc4b6298adb583ab0e211a4682f241eb"},
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:20b0f8dc160ba83b6dcc0e256846e1a02d044e13f7ea74a3d1d56ede4e48c632"},
@@ -5691,7 +5602,6 @@ files = [
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:749c16fcc4a2b09f28843cda5a193e0283e47454b63ec4b81eaa2242f50e4ccd"},
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bf165fef1f223beae7333275156ab2022cffe255dcc51c27f066b4370da81e31"},
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:32621c177bbf782ca5a18ba4d7af0f1082a3f6e517ac2a18b3974d4edf349680"},
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b82a7c94a498853aa0b272fd5bc67f29008da798d4f93a2f9f289feb8426a58d"},
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win32.whl", hash = "sha256:e8c4ebfcfd57177b572e2040777b8abc537cdef58a2120e830124946aa9b42c5"},
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win_amd64.whl", hash = "sha256:0467c5965282c62203273b838ae77c0d29d7638c8a4e3a1c8bdd3602c10904e4"},
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4c8c5d82f50bb53986a5e02d1b3092b03622c02c2eb78e29bec33fd9593bae1a"},
@@ -5700,7 +5610,6 @@ files = [
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96777d473c05ee3e5e3c3e999f5d23c6f4ec5b0c38c098b3a5229085f74236c6"},
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:3bc2a80e6420ca8b7d3590791e2dfc709c88ab9152c00eeb511c9875ce5778bf"},
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e188d2699864c11c36cdfdada94d781fd5d6b0071cd9c427bceb08ad3d7c70e1"},
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f6f3eac23941b32afccc23081e1f50612bdbe4e982012ef4f5797986828cd01"},
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win32.whl", hash = "sha256:6442cb36270b3afb1b4951f060eccca1ce49f3d087ca1ca4563a6eb479cb3de6"},
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win_amd64.whl", hash = "sha256:e5b8daf27af0b90da7bb903a876477a9e6d7270be6146906b276605997c7e9a3"},
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:fc4b630cd3fa2cf7fce38afa91d7cfe844a9f75d7f0f36393fa98815e911d987"},
@@ -5709,7 +5618,6 @@ files = [
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2f1c3765db32be59d18ab3953f43ab62a761327aafc1594a2a1fbe038b8b8a7"},
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d85252669dc32f98ebcd5d36768f5d4faeaeaa2d655ac0473be490ecdae3c285"},
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e143ada795c341b56de9418c58d028989093ee611aa27ffb9b7f609c00d813ed"},
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2c59aa6170b990d8d2719323e628aaf36f3bfbc1c26279c0eeeb24d05d2d11c7"},
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win32.whl", hash = "sha256:beffaed67936fbbeffd10966a4eb53c402fafd3d6833770516bf7314bc6ffa12"},
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win_amd64.whl", hash = "sha256:040ae85536960525ea62868b642bdb0c2cc6021c9f9d507810c0c604e66f5a7b"},
{file = "ruamel.yaml.clib-0.2.12.tar.gz", hash = "sha256:6c8fbb13ec503f99a91901ab46e0b07ae7941cd527393187039aec586fdfd36f"},
@@ -6296,7 +6204,6 @@ description = "HTTP library with thread-safe connection pooling, file post, and
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7"
groups = ["main", "dev", "docs"]
markers = "python_version < \"3.10\""
files = [
{file = "urllib3-1.26.20-py2.py3-none-any.whl", hash = "sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e"},
{file = "urllib3-1.26.20.tar.gz", hash = "sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32"},
@@ -6307,25 +6214,6 @@ brotli = ["brotli (==1.0.9) ; os_name != \"nt\" and python_version < \"3\" and p
secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress ; python_version == \"2.7\"", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"]
socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
[[package]]
name = "urllib3"
version = "2.5.0"
description = "HTTP library with thread-safe connection pooling, file post, and more."
optional = false
python-versions = ">=3.9"
groups = ["main", "dev", "docs"]
markers = "python_version >= \"3.10\""
files = [
{file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"},
{file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"},
]
[package.extras]
brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""]
h2 = ["h2 (>=4,<5)"]
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
zstd = ["zstandard (>=0.18.0)"]
[[package]]
name = "virtualenv"
version = "20.31.2"
@@ -6708,4 +6596,4 @@ type = ["pytest-mypy"]
[metadata]
lock-version = "2.1"
python-versions = ">3.9.1,<3.13"
content-hash = "d72c55b52949ba94f0c68004d5b778edb69514a05bbb7aba8d641b5058a99fd5"
content-hash = "c442552635c8e904d1c7a50f4787c8e90ec90787960ee1867f2235a7aa2205f0"
+1
View File
@@ -40,6 +40,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
- `keyvault_ensure_public_network_access_disabled` check for Azure provider. [(#8072)](https://github.com/prowler-cloud/prowler/pull/8072)
- New check `monitor_alert_service_health_exists` for Azure provider [(#8067)](https://github.com/prowler-cloud/prowler/pull/8067)
- Replace `Domain.Read.All` with `Directory.Read.All` in Azure and M365 docs [(#8075)](https://github.com/prowler-cloud/prowler/pull/8075)
- Refactor IaC provider to use Checkov as Python library [(#8093)](https://github.com/prowler-cloud/prowler/pull/8093)
### Fixed
- Consolidate Azure Storage file service properties to the account level, improving the accuracy of the `storage_ensure_file_shares_soft_delete_is_enabled` check [(#8087)](https://github.com/prowler-cloud/prowler/pull/8087)
+1 -16
View File
@@ -28,6 +28,7 @@ Mutelist:
Tags:
- "test=test" # Will ignore every resource containing the string "test" and the tags 'test=test' and
- "project=test|project=stage" # either of ('project=test' OR project=stage) in account 123456789012 and every region
- "environment=prod" # Will ignore every resource except in account 123456789012 except the ones containing the string "test" and tag environment=prod
"*":
Checks:
@@ -46,9 +47,6 @@ Mutelist:
- "*"
Tags:
- "environment=dev" # Will ignore every resource containing the tag 'environment=dev' in every account and region
"*":
Checks:
"ecs_task_definitions_no_environment_secrets":
Regions:
- "*"
@@ -60,16 +58,3 @@ Mutelist:
Regions:
- "eu-west-1"
- "eu-south-2" # Will ignore every resource in check ecs_task_definitions_no_environment_secrets except the ones in account 0123456789012 located in eu-south-2 or eu-west-1
"123456789012":
Checks:
"*":
Regions:
- "*"
Resources:
- "*"
Exceptions:
Resources:
- "test"
Tags:
- "environment=prod" # Will ignore every resource except in account 123456789012 except the ones containing the string "test" and tag environment=prod
@@ -28,9 +28,6 @@ Mutelist:
Tags:
- "test=test" # Will ignore every resource containing the string "test" and the tags 'test=test' and
- "project=test|project=stage" # either of ('project=test' OR project=stage) in Azure subscription 1 and every location
"*":
Checks:
"admincenter_*":
Regions:
- "*"
+8 -6
View File
@@ -7,6 +7,7 @@ from dataclasses import asdict, dataclass, is_dataclass
from enum import Enum
from typing import Any, Dict, Optional, Set
from checkov.common.output.record import Record
from pydantic.v1 import BaseModel, ValidationError, validator
from prowler.config.config import Provider
@@ -441,6 +442,8 @@ class Check_Report:
self.resource = resource.to_dict()
elif is_dataclass(resource):
self.resource = asdict(resource)
elif hasattr(resource, "__dict__"):
self.resource = resource.__dict__
else:
logger.error(
f"Resource metadata {type(resource)} in {self.check_metadata.CheckID} could not be converted to dict"
@@ -622,7 +625,7 @@ class CheckReportIAC(Check_Report):
resource_path: str
resource_line_range: str
def __init__(self, metadata: dict = {}, finding: dict = {}) -> None:
def __init__(self, metadata: dict = {}, resource: Record = None) -> None:
"""
Initialize the IAC Check's finding information from a Checkov failed_check dict.
@@ -630,11 +633,10 @@ class CheckReportIAC(Check_Report):
metadata (Dict): Optional check metadata (can be None).
failed_check (dict): A single failed_check result from Checkov's JSON output.
"""
super().__init__(metadata, finding)
self.resource_name = getattr(finding, "resource", "")
self.resource_path = getattr(finding, "file_path", "")
self.resource_line_range = getattr(finding, "file_line_range", "")
super().__init__(metadata, resource)
self.resource_name = resource.resource
self.resource_path = resource.file_path
self.resource_line_range = resource.file_line_range
@dataclass
+2 -2
View File
@@ -286,8 +286,8 @@ class Finding(BaseModel):
output_data["auth_method"] = "local" # Until we support remote repos
output_data["account_uid"] = "iac"
output_data["account_name"] = "iac"
output_data["resource_name"] = check_output.resource["resource"]
output_data["resource_uid"] = check_output.resource["resource"]
output_data["resource_name"] = check_output.resource_name
output_data["resource_uid"] = check_output.resource_name
output_data["region"] = check_output.resource_path
output_data["resource_line_range"] = check_output.resource_line_range
output_data["framework"] = (
+2
View File
@@ -246,6 +246,8 @@ class Provider(ABC):
elif "iac" in provider_class_name.lower():
provider_class(
scan_path=arguments.scan_path,
frameworks=arguments.frameworks,
exclude_path=arguments.exclude_path,
config_path=arguments.config_file,
fixer_config=fixer_config,
)
+136 -91
View File
@@ -1,8 +1,39 @@
import json
import subprocess
import sys
from typing import List
from checkov.ansible.runner import Runner as AnsibleRunner
from checkov.argo_workflows.runner import Runner as ArgoWorkflowsRunner
from checkov.arm.runner import Runner as ArmRunner
from checkov.azure_pipelines.runner import Runner as AzurePipelinesRunner
from checkov.bicep.runner import Runner as BicepRunner
from checkov.bitbucket.runner import Runner as BitbucketRunner
from checkov.bitbucket_pipelines.runner import Runner as BitbucketPipelinesRunner
from checkov.cdk.runner import CdkRunner
from checkov.circleci_pipelines.runner import Runner as CircleciPipelinesRunner
from checkov.cloudformation.runner import Runner as CfnRunner
from checkov.common.output.record import Record
from checkov.common.output.report import Report
from checkov.common.runners.runner_registry import RunnerRegistry
from checkov.dockerfile.runner import Runner as DockerfileRunner
from checkov.github.runner import Runner as GithubRunner
from checkov.github_actions.runner import Runner as GithubActionsRunner
from checkov.gitlab.runner import Runner as GitlabRunner
from checkov.gitlab_ci.runner import Runner as GitlabCiRunner
from checkov.helm.runner import Runner as HelmRunner
from checkov.json_doc.runner import Runner as JsonDocRunner
from checkov.kubernetes.runner import Runner as K8sRunner
from checkov.kustomize.runner import Runner as KustomizeRunner
from checkov.openapi.runner import Runner as OpenapiRunner
from checkov.runner_filter import RunnerFilter
from checkov.sast.runner import Runner as SastRunner
from checkov.sca_image.runner import Runner as ScaImageRunner
from checkov.sca_package_2.runner import Runner as ScaPackage2Runner
from checkov.secrets.runner import Runner as SecretsRunner
from checkov.serverless.runner import Runner as ServerlessRunner
from checkov.terraform.runner import Runner as TerraformRunner
from checkov.terraform_json.runner import TerraformJsonRunner
from checkov.yaml_doc.runner import Runner as YamlDocRunner
from colorama import Fore, Style
from prowler.config.config import (
@@ -23,6 +54,8 @@ class IacProvider(Provider):
def __init__(
self,
scan_path: str = ".",
frameworks: list[str] = ["all"],
exclude_path: list[str] = [],
config_path: str = None,
config_content: dict = None,
fixer_config: dict = {},
@@ -30,6 +63,8 @@ class IacProvider(Provider):
logger.info("Instantiating IAC Provider...")
self.scan_path = scan_path
self.frameworks = frameworks
self.exclude_path = exclude_path
self.region = "global"
self.audited_account = "local-iac"
self._session = None
@@ -86,7 +121,9 @@ class IacProvider(Provider):
"""IAC provider doesn't need a session since it uses Checkov directly"""
return None
def _process_check(self, finding: dict, check: dict, status: str) -> CheckReportIAC:
def _process_check(
self, finding: Report, check: Record, status: str
) -> CheckReportIAC:
"""
Process a single check (failed or passed) and create a CheckReportIAC object.
@@ -98,109 +135,110 @@ class IacProvider(Provider):
Returns:
CheckReportIAC: The processed check report
"""
metadata_dict = {
"Provider": "iac",
"CheckID": check.get("check_id", ""),
"CheckTitle": check.get("check_name", ""),
"CheckType": ["Infrastructure as Code"],
"ServiceName": finding["check_type"],
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": (
check.get("severity", "low").lower() if check.get("severity") else "low"
),
"ResourceType": "iac",
"Description": check.get("check_name", ""),
"Risk": "",
"RelatedUrl": (
check.get("guideline", "") if check.get("guideline") else ""
),
"Remediation": {
"Code": {
"NativeIaC": "",
"Terraform": "",
"CLI": "",
"Other": "",
try:
metadata_dict = {
"Provider": "iac",
"CheckID": check.check_id,
"CheckTitle": check.check_name,
"CheckType": ["Infrastructure as Code"],
"ServiceName": finding.check_type,
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": (check.severity.lower() if check.severity else "low"),
"ResourceType": finding.check_type,
"Description": check.check_name,
"Risk": "",
"RelatedUrl": (check.guideline if check.guideline else ""),
"Remediation": {
"Code": {
"NativeIaC": "",
"Terraform": "",
"CLI": "",
"Other": "",
},
"Recommendation": {
"Text": "",
"Url": (check.guideline if check.guideline else ""),
},
},
"Recommendation": {
"Text": "",
"Url": (
check.get("guideline", "") if check.get("guideline") else ""
),
},
},
"Categories": [],
"DependsOn": [],
"RelatedTo": [],
"Notes": "",
}
"Categories": [],
"DependsOn": [],
"RelatedTo": [],
"Notes": "",
}
# Convert metadata dict to JSON string
metadata = json.dumps(metadata_dict)
# Convert metadata dict to JSON string
metadata = json.dumps(metadata_dict)
report = CheckReportIAC(metadata=metadata, finding=check)
report.status = status
report.resource_tags = check.get("entity_tags", {})
report.status_extended = check.get("check_name", "")
if status == "MUTED":
report.muted = True
return report
report = CheckReportIAC(metadata=metadata, resource=check)
report.status = status
report.resource_tags = check.entity_tags
report.status_extended = check.check_name
if status == "MUTED":
report.muted = True
return report
except Exception as error:
logger.critical(
f"{error.__class__.__name__}:{error.__traceback__.tb_lineno} -- {error}"
)
sys.exit(1)
def run(self) -> List[CheckReportIAC]:
return self.run_scan(self.scan_path)
return self.run_scan(self.scan_path, self.frameworks, self.exclude_path)
def run_scan(self, directory: str) -> List[CheckReportIAC]:
def run_scan(
self, directory: str, frameworks: list[str], exclude_path: list[str]
) -> List[CheckReportIAC]:
try:
logger.info(f"Running IaC scan on {directory}...")
# Run Checkov with JSON output
process = subprocess.run(
["checkov", "-d", directory, "-o", "json"],
capture_output=True,
text=True,
runners = [
TerraformRunner(),
CfnRunner(),
K8sRunner(),
ArmRunner(),
ServerlessRunner(),
DockerfileRunner(),
YamlDocRunner(),
OpenapiRunner(),
SastRunner(),
ScaImageRunner(),
ScaPackage2Runner(),
SecretsRunner(),
AnsibleRunner(),
ArgoWorkflowsRunner(),
BitbucketRunner(),
BitbucketPipelinesRunner(),
CdkRunner(),
CircleciPipelinesRunner(),
GithubRunner(),
GithubActionsRunner(),
GitlabRunner(),
GitlabCiRunner(),
HelmRunner(),
JsonDocRunner(),
TerraformJsonRunner(),
KustomizeRunner(),
AzurePipelinesRunner(),
BicepRunner(),
]
runner_filter = RunnerFilter(
framework=frameworks, excluded_paths=exclude_path
)
# Log Checkov's error output if any
if process.stderr:
logger.error(process.stderr)
try:
output = json.loads(process.stdout)
if not output:
logger.warning("No findings returned from Checkov scan")
return []
except Exception as error:
logger.critical(
f"{error.__class__.__name__}:{error.__traceback__.tb_lineno} -- {error}"
)
sys.exit(1)
registry = RunnerRegistry("", runner_filter, *runners)
checkov_reports = registry.run(root_folder=directory)
reports = []
reports: List[CheckReportIAC] = []
for report in checkov_reports:
# If only one framework has findings, the output is a dict, otherwise it's a list of dicts
if isinstance(output, dict):
output = [output]
for failed in report.failed_checks:
reports.append(self._process_check(report, failed, "FAIL"))
# Process all frameworks findings
for finding in output:
results = finding.get("results", {})
for passed in report.passed_checks:
reports.append(self._process_check(report, passed, "PASS"))
# Process failed checks
failed_checks = results.get("failed_checks", [])
for failed_check in failed_checks:
report = self._process_check(finding, failed_check, "FAIL")
reports.append(report)
# Process passed checks
passed_checks = results.get("passed_checks", [])
for passed_check in passed_checks:
report = self._process_check(finding, passed_check, "PASS")
reports.append(report)
# Process skipped checks (muted)
skipped_checks = results.get("skipped_checks", [])
for skipped_check in skipped_checks:
report = self._process_check(finding, skipped_check, "MUTED")
reports.append(report)
for skipped in report.skipped_checks:
reports.append(self._process_check(report, skipped, "MUTED"))
return reports
@@ -214,5 +252,12 @@ class IacProvider(Provider):
report_lines = [
f"Directory: {Fore.YELLOW}{self.scan_path}{Style.RESET_ALL}",
]
if self.exclude_path:
report_lines.append(
f"Excluded paths: {Fore.YELLOW}{', '.join(self.exclude_path)}{Style.RESET_ALL}"
)
report_lines.append(
f"Frameworks: {Fore.YELLOW}{', '.join(self.frameworks)}{Style.RESET_ALL}"
)
report_title = f"{Style.BRIGHT}Scanning local IaC directory:{Style.RESET_ALL}"
print_boxes(report_lines, report_title)
@@ -1,3 +1,36 @@
FRAMEWORK_CHOICES = [
"ansible",
"argo_workflows",
"arm",
"azure_pipelines",
"bicep",
"bitbucket",
"bitbucket_pipelines",
"cdk",
"circleci_pipelines",
"cloudformation",
"dockerfile",
"github",
"github_actions",
"gitlab",
"gitlab_ci",
"helm",
"json_doc",
"kubernetes",
"kustomize",
"openapi",
"policies_3d",
"sast",
"sca_image",
"sca_package_2",
"secrets",
"serverless",
"terraform",
"terraform_json",
"yaml_doc",
]
def init_parser(self):
"""Init the IAC Provider CLI parser"""
iac_parser = self.subparsers.add_parser(
@@ -13,3 +46,20 @@ def init_parser(self):
default=".",
help="Path to the folder containing your infrastructure-as-code files. Default: current directory",
)
iac_scan_subparser.add_argument(
"--frameworks",
"-f",
"--framework",
dest="frameworks",
nargs="+",
default=["all"],
choices=FRAMEWORK_CHOICES,
help="Comma-separated list of frameworks to scan. Default: all",
)
iac_scan_subparser.add_argument(
"--exclude-path",
dest="exclude_path",
nargs="+",
default=[],
help="Comma-separated list of paths to exclude from the scan. Default: none",
)
+1 -1
View File
@@ -59,7 +59,7 @@ dependencies = [
"slack-sdk==3.34.0",
"tabulate==0.9.0",
"tzlocal==5.3.1",
"checkov (>=3.2.434,<4.0.0)",
"checkov==3.2.445",
"py-iam-expand==0.1.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."
+244 -54
View File
@@ -1,67 +1,257 @@
from checkov.common.models.enums import CheckResult
from checkov.common.output.record import Record
from checkov.common.output.report import Report
# IAC Provider Constants
DEFAULT_SCAN_PATH = "."
# Sample Checkov Output
SAMPLE_CHECKOV_OUTPUT = [
{
"check_type": "terraform",
"results": {
"failed_checks": [
{
"check_id": "CKV_AWS_1",
"check_name": "Ensure S3 bucket has encryption enabled",
"guideline": "https://docs.bridgecrew.io/docs/s3_1-s3-bucket-has-encryption-enabled",
"severity": "low",
},
{
"check_id": "CKV_AWS_2",
"check_name": "Ensure S3 bucket has public access blocked",
"guideline": "https://docs.bridgecrew.io/docs/s3_2-s3-bucket-has-public-access-blocked",
"severity": "low",
},
],
"passed_checks": [
{
"check_id": "CKV_AWS_3",
"check_name": "Ensure S3 bucket has versioning enabled",
"guideline": "https://docs.bridgecrew.io/docs/s3_3-s3-bucket-has-versioning-enabled",
"severity": "low",
}
],
},
}
]
# Sample Finding Data
SAMPLE_FINDING = SAMPLE_CHECKOV_OUTPUT[0]
SAMPLE_FINDING = Report(check_type="terraform")
SAMPLE_FAILED_CHECK = Record(
check_id="CKV_AWS_1",
check_name="Ensure S3 bucket has encryption enabled",
severity="low",
file_path="test.tf",
file_line_range=[1, 2],
resource="aws_s3_bucket.test_bucket",
evaluations=[],
check_class="terraform",
check_result=CheckResult.FAILED,
code_block=[],
file_abs_path="test.tf",
)
SAMPLE_FAILED_CHECK.guideline = (
"https://docs.bridgecrew.io/docs/s3_1-s3-bucket-has-encryption-enabled"
)
SAMPLE_FAILED_CHECK = {
"check_id": "CKV_AWS_1",
"check_name": "Ensure S3 bucket has encryption enabled",
"guideline": "https://docs.bridgecrew.io/docs/s3_1-s3-bucket-has-encryption-enabled",
"severity": "low",
}
SAMPLE_PASSED_CHECK = Record(
check_id="CKV_AWS_3",
check_name="Ensure S3 bucket has versioning enabled",
severity="low",
file_path="test.tf",
file_line_range=[1, 2],
resource="aws_s3_bucket.test_bucket",
evaluations=[],
check_class="terraform",
check_result=CheckResult.PASSED,
code_block=[],
file_abs_path="test.tf",
)
SAMPLE_PASSED_CHECK.guideline = (
"https://docs.bridgecrew.io/docs/s3_3-s3-bucket-has-versioning-enabled"
)
SAMPLE_PASSED_CHECK = {
"check_id": "CKV_AWS_3",
"check_name": "Ensure S3 bucket has versioning enabled",
"guideline": "https://docs.bridgecrew.io/docs/s3_3-s3-bucket-has-versioning-enabled",
"severity": "low",
}
# Additional test fixtures for comprehensive testing
SAMPLE_SKIPPED_CHECK = Record(
check_id="CKV_AWS_2",
check_name="Ensure S3 bucket has public access blocked",
severity="high",
file_path="test.tf",
file_line_range=[3, 4],
resource="aws_s3_bucket.test_bucket",
evaluations=[],
check_class="terraform",
check_result=CheckResult.SKIPPED,
code_block=[],
file_abs_path="test.tf",
)
SAMPLE_SKIPPED_CHECK.guideline = (
"https://docs.bridgecrew.io/docs/s3_2-s3-bucket-has-public-access-blocked"
)
SAMPLE_HIGH_SEVERITY_CHECK = Record(
check_id="CKV_AWS_4",
check_name="Ensure S3 bucket has logging enabled",
severity="HIGH",
file_path="test.tf",
file_line_range=[5, 6],
resource="aws_s3_bucket.test_bucket",
evaluations=[],
check_class="terraform",
check_result=CheckResult.FAILED,
code_block=[],
file_abs_path="test.tf",
)
SAMPLE_HIGH_SEVERITY_CHECK.guideline = (
"https://docs.bridgecrew.io/docs/s3_4-s3-bucket-has-logging-enabled"
)
def get_sample_checkov_json_output():
"""Return sample Checkov JSON output as string"""
import json
SAMPLE_KUBERNETES_CHECK = Record(
check_id="CKV_K8S_1",
check_name="Ensure API server has audit logging enabled",
severity="medium",
file_path="deployment.yaml",
file_line_range=[1, 10],
resource="kubernetes_deployment.test_deployment",
evaluations=[],
check_class="kubernetes",
check_result=CheckResult.FAILED,
code_block=[],
file_abs_path="deployment.yaml",
)
SAMPLE_KUBERNETES_CHECK.guideline = (
"https://docs.bridgecrew.io/docs/k8s_1-api-server-has-audit-logging-enabled"
)
return json.dumps(SAMPLE_CHECKOV_OUTPUT)
SAMPLE_CLOUDFORMATION_CHECK = Record(
check_id="CKV_AWS_5",
check_name="Ensure CloudFormation stacks are not publicly accessible",
severity="critical",
file_path="template.yaml",
file_line_range=[1, 20],
resource="AWS::CloudFormation::Stack",
evaluations=[],
check_class="cloudformation",
check_result=CheckResult.PASSED,
code_block=[],
file_abs_path="template.yaml",
)
SAMPLE_CLOUDFORMATION_CHECK.guideline = "https://docs.bridgecrew.io/docs/cfn_1-cloudformation-stacks-are-not-publicly-accessible"
# Sample findings for different frameworks
SAMPLE_KUBERNETES_FINDING = Report(check_type="kubernetes")
SAMPLE_CLOUDFORMATION_FINDING = Report(check_type="cloudformation")
def get_empty_checkov_output():
"""Return empty Checkov output as string"""
return "[]"
# Additional fixtures for different test scenarios
SAMPLE_CHECK_WITHOUT_GUIDELINE = Record(
check_id="CKV_AWS_6",
check_name="Test check without guideline",
severity="low",
file_path="test.tf",
file_line_range=[1, 2],
resource="aws_s3_bucket.test_bucket",
evaluations=[],
check_class="terraform",
check_result=CheckResult.FAILED,
code_block=[],
file_abs_path="test.tf",
)
# Note: No guideline attribute set
SAMPLE_MEDIUM_SEVERITY_CHECK = Record(
check_id="CKV_AWS_7",
check_name="Ensure S3 bucket has proper access controls",
severity="MEDIUM",
file_path="test.tf",
file_line_range=[7, 8],
resource="aws_s3_bucket.test_bucket",
evaluations=[],
check_class="terraform",
check_result=CheckResult.FAILED,
code_block=[],
file_abs_path="test.tf",
)
SAMPLE_MEDIUM_SEVERITY_CHECK.guideline = (
"https://docs.bridgecrew.io/docs/s3_7-s3-bucket-has-proper-access-controls"
)
def get_invalid_checkov_output():
"""Return invalid JSON output as string"""
return "invalid json output"
SAMPLE_CRITICAL_SEVERITY_CHECK = Record(
check_id="CKV_AWS_8",
check_name="Ensure S3 bucket has encryption at rest",
severity="CRITICAL",
file_path="test.tf",
file_line_range=[9, 10],
resource="aws_s3_bucket.test_bucket",
evaluations=[],
check_class="terraform",
check_result=CheckResult.FAILED,
code_block=[],
file_abs_path="test.tf",
)
SAMPLE_CRITICAL_SEVERITY_CHECK.guideline = (
"https://docs.bridgecrew.io/docs/s3_8-s3-bucket-has-encryption-at-rest"
)
# Sample reports for different frameworks
SAMPLE_TERRAFORM_REPORT = Report(check_type="terraform")
SAMPLE_KUBERNETES_REPORT = Report(check_type="kubernetes")
SAMPLE_CLOUDFORMATION_REPORT = Report(check_type="cloudformation")
SAMPLE_DOCKERFILE_REPORT = Report(check_type="dockerfile")
SAMPLE_YAML_REPORT = Report(check_type="yaml")
# Sample checks for different frameworks
SAMPLE_DOCKERFILE_CHECK = Record(
check_id="CKV_DOCKER_1",
check_name="Ensure base image is not using latest tag",
severity="high",
file_path="Dockerfile",
file_line_range=[1, 1],
resource="Dockerfile",
evaluations=[],
check_class="dockerfile",
check_result=CheckResult.FAILED,
code_block=[],
file_abs_path="Dockerfile",
)
SAMPLE_DOCKERFILE_CHECK.guideline = (
"https://docs.bridgecrew.io/docs/docker_1-base-image-not-using-latest-tag"
)
SAMPLE_YAML_CHECK = Record(
check_id="CKV_YAML_1",
check_name="Ensure YAML file has proper indentation",
severity="low",
file_path="config.yaml",
file_line_range=[1, 5],
resource="config.yaml",
evaluations=[],
check_class="yaml",
check_result=CheckResult.PASSED,
code_block=[],
file_abs_path="config.yaml",
)
SAMPLE_YAML_CHECK.guideline = (
"https://docs.bridgecrew.io/docs/yaml_1-proper-indentation"
)
# Sample checks with different statuses for comprehensive testing
SAMPLE_ANOTHER_FAILED_CHECK = Record(
check_id="CKV_AWS_9",
check_name="Ensure S3 bucket has lifecycle policy",
severity="medium",
file_path="test.tf",
file_line_range=[11, 12],
resource="aws_s3_bucket.test_bucket",
evaluations=[],
check_class="terraform",
check_result=CheckResult.FAILED,
code_block=[],
file_abs_path="test.tf",
)
SAMPLE_ANOTHER_FAILED_CHECK.guideline = (
"https://docs.bridgecrew.io/docs/s3_9-s3-bucket-has-lifecycle-policy"
)
SAMPLE_ANOTHER_PASSED_CHECK = Record(
check_id="CKV_AWS_10",
check_name="Ensure S3 bucket has proper tags",
severity="low",
file_path="test.tf",
file_line_range=[13, 14],
resource="aws_s3_bucket.test_bucket",
evaluations=[],
check_class="terraform",
check_result=CheckResult.PASSED,
code_block=[],
file_abs_path="test.tf",
)
SAMPLE_ANOTHER_PASSED_CHECK.guideline = (
"https://docs.bridgecrew.io/docs/s3_10-s3-bucket-has-proper-tags"
)
SAMPLE_ANOTHER_SKIPPED_CHECK = Record(
check_id="CKV_AWS_11",
check_name="Ensure S3 bucket has cross-region replication",
severity="high",
file_path="test.tf",
file_line_range=[15, 16],
resource="aws_s3_bucket.test_bucket",
evaluations=[],
check_class="terraform",
check_result=CheckResult.SKIPPED,
code_block=[],
file_abs_path="test.tf",
)
SAMPLE_ANOTHER_SKIPPED_CHECK.guideline = (
"https://docs.bridgecrew.io/docs/s3_11-s3-bucket-has-cross-region-replication"
)
+468 -55
View File
@@ -1,4 +1,4 @@
from unittest.mock import MagicMock, patch
from unittest.mock import Mock, patch
import pytest
@@ -6,12 +6,24 @@ from prowler.lib.check.models import CheckReportIAC
from prowler.providers.iac.iac_provider import IacProvider
from tests.providers.iac.iac_fixtures import (
DEFAULT_SCAN_PATH,
SAMPLE_ANOTHER_FAILED_CHECK,
SAMPLE_ANOTHER_PASSED_CHECK,
SAMPLE_ANOTHER_SKIPPED_CHECK,
SAMPLE_CHECK_WITHOUT_GUIDELINE,
SAMPLE_CLOUDFORMATION_CHECK,
SAMPLE_CRITICAL_SEVERITY_CHECK,
SAMPLE_DOCKERFILE_CHECK,
SAMPLE_DOCKERFILE_REPORT,
SAMPLE_FAILED_CHECK,
SAMPLE_FINDING,
SAMPLE_HIGH_SEVERITY_CHECK,
SAMPLE_KUBERNETES_CHECK,
SAMPLE_KUBERNETES_FINDING,
SAMPLE_MEDIUM_SEVERITY_CHECK,
SAMPLE_PASSED_CHECK,
get_empty_checkov_output,
get_invalid_checkov_output,
get_sample_checkov_json_output,
SAMPLE_SKIPPED_CHECK,
SAMPLE_YAML_CHECK,
SAMPLE_YAML_REPORT,
)
@@ -46,10 +58,10 @@ class TestIacProvider:
assert report.status == "FAIL"
assert report.check_metadata.Provider == "iac"
assert report.check_metadata.CheckID == SAMPLE_FAILED_CHECK["check_id"]
assert report.check_metadata.CheckTitle == SAMPLE_FAILED_CHECK["check_name"]
assert report.check_metadata.CheckID == SAMPLE_FAILED_CHECK.check_id
assert report.check_metadata.CheckTitle == SAMPLE_FAILED_CHECK.check_name
assert report.check_metadata.Severity == "low"
assert report.check_metadata.RelatedUrl == SAMPLE_FAILED_CHECK["guideline"]
assert report.check_metadata.RelatedUrl == SAMPLE_FAILED_CHECK.guideline
def test_iac_provider_process_check_passed(self):
"""Test processing a passed check"""
@@ -61,72 +73,473 @@ class TestIacProvider:
assert report.status == "PASS"
assert report.check_metadata.Provider == "iac"
assert report.check_metadata.CheckID == SAMPLE_PASSED_CHECK["check_id"]
assert report.check_metadata.CheckTitle == SAMPLE_PASSED_CHECK["check_name"]
assert report.check_metadata.CheckID == SAMPLE_PASSED_CHECK.check_id
assert report.check_metadata.CheckTitle == SAMPLE_PASSED_CHECK.check_name
assert report.check_metadata.Severity == "low"
assert report.check_metadata.RelatedUrl == SAMPLE_PASSED_CHECK.guideline
@patch("subprocess.run")
def test_iac_provider_run_scan_success(self, mock_subprocess):
"""Test successful IAC scan with Checkov"""
def test_iac_provider_process_check_skipped(self):
"""Test processing a skipped check"""
provider = IacProvider()
mock_subprocess.return_value = MagicMock(
stdout=get_sample_checkov_json_output(), stderr=""
)
report = provider._process_check(SAMPLE_FINDING, SAMPLE_SKIPPED_CHECK, "MUTED")
reports = provider.run_scan("/test/directory")
assert isinstance(report, CheckReportIAC)
assert report.status == "MUTED"
assert report.muted is True
# Should have 2 failed checks + 1 passed check = 3 total reports
assert len(reports) == 3
assert report.check_metadata.Provider == "iac"
assert report.check_metadata.CheckID == SAMPLE_SKIPPED_CHECK.check_id
assert report.check_metadata.CheckTitle == SAMPLE_SKIPPED_CHECK.check_name
assert report.check_metadata.Severity == "high"
assert report.check_metadata.RelatedUrl == SAMPLE_SKIPPED_CHECK.guideline
# Check that we have both failed and passed reports
failed_reports = [r for r in reports if r.status == "FAIL"]
passed_reports = [r for r in reports if r.status == "PASS"]
assert len(failed_reports) == 2
assert len(passed_reports) == 1
# Verify subprocess was called correctly
mock_subprocess.assert_called_once_with(
["checkov", "-d", "/test/directory", "-o", "json"],
capture_output=True,
text=True,
)
@patch("subprocess.run")
def test_iac_provider_run_scan_empty_output(self, mock_subprocess):
"""Test IAC scan with empty Checkov output"""
def test_iac_provider_process_check_high_severity(self):
"""Test processing a high severity check"""
provider = IacProvider()
mock_subprocess.return_value = MagicMock(
stdout=get_empty_checkov_output(), stderr=""
report = provider._process_check(
SAMPLE_FINDING, SAMPLE_HIGH_SEVERITY_CHECK, "FAIL"
)
reports = provider.run_scan("/test/directory")
assert isinstance(report, CheckReportIAC)
assert report.status == "FAIL"
assert report.check_metadata.Severity == "high"
assert len(reports) == 0
@patch("subprocess.run")
def test_iac_provider_run_scan_invalid_json(self, mock_subprocess):
"""Test IAC scan with invalid JSON output"""
def test_iac_provider_process_check_different_framework(self):
"""Test processing a check from a different framework (Kubernetes)"""
provider = IacProvider()
mock_subprocess.return_value = MagicMock(
stdout=get_invalid_checkov_output(), stderr=""
report = provider._process_check(
SAMPLE_KUBERNETES_FINDING, SAMPLE_KUBERNETES_CHECK, "FAIL"
)
with pytest.raises(SystemExit) as excinfo:
provider.run_scan("/test/directory")
assert isinstance(report, CheckReportIAC)
assert report.status == "FAIL"
assert report.check_metadata.ServiceName == "kubernetes"
assert report.check_metadata.CheckID == SAMPLE_KUBERNETES_CHECK.check_id
assert excinfo.value.code == 1
@patch("subprocess.run")
def test_iac_provider_run_scan_null_output(self, mock_subprocess):
"""Test IAC scan with null Checkov output"""
def test_iac_provider_process_check_no_guideline(self):
"""Test processing a check without guideline URL"""
provider = IacProvider()
mock_subprocess.return_value = MagicMock(stdout="null", stderr="")
report = provider._process_check(
SAMPLE_FINDING, SAMPLE_CHECK_WITHOUT_GUIDELINE, "FAIL"
)
reports = provider.run_scan("/test/directory")
assert isinstance(report, CheckReportIAC)
assert report.status == "FAIL"
assert report.check_metadata.RelatedUrl == ""
assert len(reports) == 0
def test_iac_provider_process_check_medium_severity(self):
"""Test processing a medium severity check"""
provider = IacProvider()
report = provider._process_check(
SAMPLE_FINDING, SAMPLE_MEDIUM_SEVERITY_CHECK, "FAIL"
)
assert isinstance(report, CheckReportIAC)
assert report.status == "FAIL"
assert report.check_metadata.Severity == "medium"
def test_iac_provider_process_check_critical_severity(self):
"""Test processing a critical severity check"""
provider = IacProvider()
report = provider._process_check(
SAMPLE_FINDING, SAMPLE_CRITICAL_SEVERITY_CHECK, "FAIL"
)
assert isinstance(report, CheckReportIAC)
assert report.status == "FAIL"
assert report.check_metadata.Severity == "critical"
def test_iac_provider_process_check_dockerfile(self):
"""Test processing a Dockerfile check"""
provider = IacProvider()
report = provider._process_check(
SAMPLE_DOCKERFILE_REPORT, SAMPLE_DOCKERFILE_CHECK, "FAIL"
)
assert isinstance(report, CheckReportIAC)
assert report.status == "FAIL"
assert report.check_metadata.ServiceName == "dockerfile"
assert report.check_metadata.CheckID == SAMPLE_DOCKERFILE_CHECK.check_id
def test_iac_provider_process_check_yaml(self):
"""Test processing a YAML check"""
provider = IacProvider()
report = provider._process_check(SAMPLE_YAML_REPORT, SAMPLE_YAML_CHECK, "PASS")
assert isinstance(report, CheckReportIAC)
assert report.status == "PASS"
assert report.check_metadata.ServiceName == "yaml"
assert report.check_metadata.CheckID == SAMPLE_YAML_CHECK.check_id
@patch("prowler.providers.iac.iac_provider.RunnerRegistry")
@patch("prowler.providers.iac.iac_provider.RunnerFilter")
@patch("prowler.providers.iac.iac_provider.logger")
def test_run_scan_success_with_failed_and_passed_checks(
self, mock_logger, mock_runner_filter, mock_runner_registry
):
"""Test successful run_scan with both failed and passed checks"""
# Setup mocks
mock_registry_instance = Mock()
mock_runner_registry.return_value = mock_registry_instance
# Create mock reports with failed and passed checks
mock_report = Mock()
mock_report.check_type = "terraform" # Set the check_type attribute
mock_report.failed_checks = [SAMPLE_FAILED_CHECK]
mock_report.passed_checks = [SAMPLE_PASSED_CHECK]
mock_report.skipped_checks = []
mock_registry_instance.run.return_value = [mock_report]
provider = IacProvider()
result = provider.run_scan("/test/directory", ["terraform"], [])
# Verify logger was called
mock_logger.info.assert_called_with("Running IaC scan on /test/directory...")
# Verify RunnerFilter was created with correct parameters
mock_runner_filter.assert_called_with(
framework=["terraform"], excluded_paths=[]
)
# Verify RunnerRegistry was created and run was called
mock_runner_registry.assert_called_once()
mock_registry_instance.run.assert_called_with(root_folder="/test/directory")
# Verify results
assert len(result) == 2
assert all(isinstance(report, CheckReportIAC) for report in result)
# Check that we have one FAIL and one PASS report
statuses = [report.status for report in result]
assert "FAIL" in statuses
assert "PASS" in statuses
@patch("prowler.providers.iac.iac_provider.RunnerRegistry")
@patch("prowler.providers.iac.iac_provider.RunnerFilter")
@patch("prowler.providers.iac.iac_provider.logger")
def test_run_scan_with_skipped_checks(
self, mock_logger, mock_runner_filter, mock_runner_registry
):
"""Test run_scan with skipped checks (muted)"""
# Setup mocks
mock_registry_instance = Mock()
mock_runner_registry.return_value = mock_registry_instance
# Create mock report with skipped checks
mock_report = Mock()
mock_report.check_type = "terraform" # Set the check_type attribute
mock_report.failed_checks = []
mock_report.passed_checks = []
mock_report.skipped_checks = [SAMPLE_SKIPPED_CHECK]
mock_registry_instance.run.return_value = [mock_report]
provider = IacProvider()
result = provider.run_scan("/test/directory", ["all"], ["exclude/path"])
# Verify RunnerFilter was created with correct parameters
mock_runner_filter.assert_called_with(
framework=["all"], excluded_paths=["exclude/path"]
)
# Verify results
assert len(result) == 1
assert isinstance(result[0], CheckReportIAC)
assert result[0].status == "MUTED"
assert result[0].muted is True
@patch("prowler.providers.iac.iac_provider.RunnerRegistry")
@patch("prowler.providers.iac.iac_provider.RunnerFilter")
@patch("prowler.providers.iac.iac_provider.logger")
def test_run_scan_empty_results(
self, mock_logger, mock_runner_filter, mock_runner_registry
):
"""Test run_scan with no findings"""
# Setup mocks
mock_registry_instance = Mock()
mock_runner_registry.return_value = mock_registry_instance
# Create mock report with no checks
mock_report = Mock()
mock_report.check_type = "terraform" # Set the check_type attribute
mock_report.failed_checks = []
mock_report.passed_checks = []
mock_report.skipped_checks = []
mock_registry_instance.run.return_value = [mock_report]
provider = IacProvider()
result = provider.run_scan("/test/directory", ["kubernetes"], [])
# Verify results
assert len(result) == 0
@patch("prowler.providers.iac.iac_provider.RunnerRegistry")
@patch("prowler.providers.iac.iac_provider.RunnerFilter")
@patch("prowler.providers.iac.iac_provider.logger")
def test_run_scan_multiple_reports(
self, mock_logger, mock_runner_filter, mock_runner_registry
):
"""Test run_scan with multiple reports from different frameworks"""
# Setup mocks
mock_registry_instance = Mock()
mock_runner_registry.return_value = mock_registry_instance
# Create multiple mock reports
mock_report1 = Mock()
mock_report1.check_type = "terraform" # Set the check_type attribute
mock_report1.failed_checks = [SAMPLE_FAILED_CHECK]
mock_report1.passed_checks = []
mock_report1.skipped_checks = []
mock_report2 = Mock()
mock_report2.check_type = "kubernetes" # Set the check_type attribute
mock_report2.failed_checks = []
mock_report2.passed_checks = [SAMPLE_PASSED_CHECK]
mock_report2.skipped_checks = []
mock_registry_instance.run.return_value = [mock_report1, mock_report2]
provider = IacProvider()
result = provider.run_scan("/test/directory", ["terraform", "kubernetes"], [])
# Verify results
assert len(result) == 2
assert all(isinstance(report, CheckReportIAC) for report in result)
# Check that we have one FAIL and one PASS report
statuses = [report.status for report in result]
assert "FAIL" in statuses
assert "PASS" in statuses
@patch("prowler.providers.iac.iac_provider.RunnerRegistry")
@patch("prowler.providers.iac.iac_provider.RunnerFilter")
@patch("prowler.providers.iac.iac_provider.logger")
@patch("prowler.providers.iac.iac_provider.sys")
def test_run_scan_exception_handling(
self, mock_sys, mock_logger, mock_runner_filter, mock_runner_registry
):
"""Test run_scan exception handling"""
# Setup mocks to raise an exception
mock_registry_instance = Mock()
mock_runner_registry.return_value = mock_registry_instance
mock_registry_instance.run.side_effect = Exception("Test exception")
# Configure sys.exit to raise SystemExit
mock_sys.exit.side_effect = SystemExit(1)
provider = IacProvider()
# The function should call sys.exit(1) when an exception occurs
with pytest.raises(SystemExit) as exc_info:
provider.run_scan("/test/directory", ["terraform"], [])
assert exc_info.value.code == 1
# Verify logger was called with error information
mock_logger.critical.assert_called_once()
critical_call_args = mock_logger.critical.call_args[0][0]
assert "Exception" in critical_call_args
assert "Test exception" in critical_call_args
@patch("prowler.providers.iac.iac_provider.RunnerRegistry")
@patch("prowler.providers.iac.iac_provider.RunnerFilter")
@patch("prowler.providers.iac.iac_provider.logger")
def test_run_scan_with_different_frameworks(
self, mock_logger, mock_runner_filter, mock_runner_registry
):
"""Test run_scan with different framework configurations"""
# Setup mocks
mock_registry_instance = Mock()
mock_runner_registry.return_value = mock_registry_instance
mock_report = Mock()
mock_report.check_type = "terraform" # Set the check_type attribute
mock_report.failed_checks = []
mock_report.passed_checks = [SAMPLE_PASSED_CHECK]
mock_report.skipped_checks = []
mock_registry_instance.run.return_value = [mock_report]
provider = IacProvider()
# Test with specific frameworks
frameworks = ["terraform", "kubernetes", "cloudformation"]
result = provider.run_scan("/test/directory", frameworks, [])
# Verify RunnerFilter was created with correct frameworks
mock_runner_filter.assert_called_with(framework=frameworks, excluded_paths=[])
# Verify results
assert len(result) == 1
assert result[0].status == "PASS"
@patch("prowler.providers.iac.iac_provider.RunnerRegistry")
@patch("prowler.providers.iac.iac_provider.RunnerFilter")
@patch("prowler.providers.iac.iac_provider.logger")
def test_run_scan_with_exclude_paths(
self, mock_logger, mock_runner_filter, mock_runner_registry
):
"""Test run_scan with exclude paths"""
# Setup mocks
mock_registry_instance = Mock()
mock_runner_registry.return_value = mock_registry_instance
mock_report = Mock()
mock_report.check_type = "terraform" # Set the check_type attribute
mock_report.failed_checks = []
mock_report.passed_checks = [SAMPLE_PASSED_CHECK]
mock_report.skipped_checks = []
mock_registry_instance.run.return_value = [mock_report]
provider = IacProvider()
# Test with exclude paths
exclude_paths = ["node_modules", ".git", "vendor"]
result = provider.run_scan("/test/directory", ["all"], exclude_paths)
# Verify RunnerFilter was created with correct exclude paths
mock_runner_filter.assert_called_with(
framework=["all"], excluded_paths=exclude_paths
)
# Verify results
assert len(result) == 1
assert result[0].status == "PASS"
@patch("prowler.providers.iac.iac_provider.RunnerRegistry")
@patch("prowler.providers.iac.iac_provider.RunnerFilter")
@patch("prowler.providers.iac.iac_provider.logger")
def test_run_scan_all_check_types(
self, mock_logger, mock_runner_filter, mock_runner_registry
):
"""Test run_scan with all types of checks (failed, passed, skipped)"""
# Setup mocks
mock_registry_instance = Mock()
mock_runner_registry.return_value = mock_registry_instance
mock_report = Mock()
mock_report.check_type = "terraform" # Set the check_type attribute
mock_report.failed_checks = [SAMPLE_FAILED_CHECK, SAMPLE_HIGH_SEVERITY_CHECK]
mock_report.passed_checks = [SAMPLE_PASSED_CHECK, SAMPLE_CLOUDFORMATION_CHECK]
mock_report.skipped_checks = [SAMPLE_SKIPPED_CHECK]
mock_registry_instance.run.return_value = [mock_report]
provider = IacProvider()
result = provider.run_scan("/test/directory", ["all"], [])
# Verify results
assert len(result) == 5 # 2 failed + 2 passed + 1 skipped
# Check status distribution
statuses = [report.status for report in result]
assert statuses.count("FAIL") == 2
assert statuses.count("PASS") == 2
assert statuses.count("MUTED") == 1
# Check that muted reports have muted=True
muted_reports = [report for report in result if report.status == "MUTED"]
assert all(report.muted for report in muted_reports)
@patch("prowler.providers.iac.iac_provider.RunnerRegistry")
@patch("prowler.providers.iac.iac_provider.RunnerFilter")
@patch("prowler.providers.iac.iac_provider.logger")
def test_run_scan_no_reports_returned(
self, mock_logger, mock_runner_filter, mock_runner_registry
):
"""Test run_scan when no reports are returned from registry"""
# Setup mocks
mock_registry_instance = Mock()
mock_runner_registry.return_value = mock_registry_instance
# Return empty list of reports
mock_registry_instance.run.return_value = []
provider = IacProvider()
result = provider.run_scan("/test/directory", ["terraform"], [])
# Verify results
assert len(result) == 0
@patch("prowler.providers.iac.iac_provider.RunnerRegistry")
@patch("prowler.providers.iac.iac_provider.RunnerFilter")
@patch("prowler.providers.iac.iac_provider.logger")
def test_run_scan_multiple_frameworks_with_different_checks(
self, mock_logger, mock_runner_filter, mock_runner_registry
):
"""Test run_scan with multiple frameworks and different types of checks"""
# Setup mocks
mock_registry_instance = Mock()
mock_runner_registry.return_value = mock_registry_instance
# Create reports for different frameworks
terraform_report = Mock()
terraform_report.check_type = "terraform"
terraform_report.failed_checks = [
SAMPLE_FAILED_CHECK,
SAMPLE_ANOTHER_FAILED_CHECK,
]
terraform_report.passed_checks = [SAMPLE_PASSED_CHECK]
terraform_report.skipped_checks = []
kubernetes_report = Mock()
kubernetes_report.check_type = "kubernetes"
kubernetes_report.failed_checks = [SAMPLE_KUBERNETES_CHECK]
kubernetes_report.passed_checks = []
kubernetes_report.skipped_checks = [SAMPLE_ANOTHER_SKIPPED_CHECK]
cloudformation_report = Mock()
cloudformation_report.check_type = "cloudformation"
cloudformation_report.failed_checks = []
cloudformation_report.passed_checks = [
SAMPLE_CLOUDFORMATION_CHECK,
SAMPLE_ANOTHER_PASSED_CHECK,
]
cloudformation_report.skipped_checks = []
mock_registry_instance.run.return_value = [
terraform_report,
kubernetes_report,
cloudformation_report,
]
provider = IacProvider()
result = provider.run_scan(
"/test/directory", ["terraform", "kubernetes", "cloudformation"], []
)
# Verify results
assert (
len(result) == 7
) # 2 failed + 1 passed (terraform) + 1 failed + 1 skipped (kubernetes) + 2 passed (cloudformation)
# Check status distribution
statuses = [report.status for report in result]
assert statuses.count("FAIL") == 3
assert statuses.count("PASS") == 3
assert statuses.count("MUTED") == 1
def test_run_method_calls_run_scan(self):
"""Test that the run method calls run_scan with correct parameters"""
provider = IacProvider(
scan_path="/custom/path", frameworks=["terraform"], exclude_path=["exclude"]
)
with patch.object(provider, "run_scan") as mock_run_scan:
mock_run_scan.return_value = []
provider.run()
mock_run_scan.assert_called_once_with(
"/custom/path", ["terraform"], ["exclude"]
)