feat(groups): Launch specific checks from groups and services (#1204)

This commit is contained in:
Pepe Fagoaga
2022-06-16 13:27:25 +02:00
committed by GitHub
parent 8abcc5988d
commit f694a6d12a
16 changed files with 201 additions and 95 deletions

View File

@@ -210,7 +210,7 @@ Prowler has been written in bash using AWS-CLI underneath and it works in Linux,
or all checks but some of them: or all checks but some of them:
```sh ```sh
./prowler -E check42,check43 ./prowler -e check42,check43
``` ```
or for custom profile and region: or for custom profile and region:
@@ -228,7 +228,7 @@ Prowler has been written in bash using AWS-CLI underneath and it works in Linux,
or exclude some checks in the group: or exclude some checks in the group:
```sh ```sh
./prowler -g group4 -E check42,check43 ./prowler -g group4 -e check42,check43
``` ```
Valid check numbers are based on the AWS CIS Benchmark guide, so 1.1 is check11 and 3.10 is check310 Valid check numbers are based on the AWS CIS Benchmark guide, so 1.1 is check11 and 3.10 is check310

0
config/__init__.py Normal file
View File

View File

@@ -2,3 +2,6 @@ from datetime import datetime
timestamp = datetime.today().strftime("%Y-%m-%d %H:%M:%S") timestamp = datetime.today().strftime("%Y-%m-%d %H:%M:%S")
prowler_version = "3.0-alfa" prowler_version = "3.0-alfa"
# Groups
groups_file = "groups.json"

8
groups.json Normal file
View File

@@ -0,0 +1,8 @@
{
"aws": {
"gdpr": [
"check11",
"check12"
]
}
}

View File

@@ -1,6 +1,6 @@
from colorama import Fore, Style from colorama import Fore, Style
from lib.config import prowler_version, timestamp from config.config import prowler_version, timestamp
def print_version(): def print_version():

View File

@@ -1,37 +1,60 @@
import importlib import importlib
import json
import pkgutil import pkgutil
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from dataclasses import dataclass from dataclasses import dataclass
from types import ModuleType
from config.config import groups_file
from lib.logger import logger from lib.logger import logger
from lib.outputs import report from lib.outputs import report
from lib.utils.utils import open_file, parse_json_file
# Exclude checks to run # Exclude checks to run
def exclude_checks_to_run(checks_to_execute, excluded_checks): def exclude_checks_to_run(checks_to_execute: set, excluded_checks: list) -> set:
for check in excluded_checks: for check in excluded_checks:
checks_to_execute.discard(check) checks_to_execute.discard(check)
return checks_to_execute return checks_to_execute
# Parse checks from file
def parse_checks_from_file(checks_file): # Load checks from checklist.json
def parse_checks_from_file(input_file: str, provider: str) -> set:
checks_to_execute = set() checks_to_execute = set()
with open(checks_file) as f: f = open_file(input_file)
for line in f: json_file = parse_json_file(f)
# Remove comments from file
line = line.partition("#")[0].strip() for check_name in json_file[provider]:
# If file contains several checks comma-separated checks_to_execute.add(check_name)
if "," in line:
for check in line.split(","):
checks_to_execute.add(check.strip())
# If line is not empty
elif len(line):
checks_to_execute.add(line)
return checks_to_execute return checks_to_execute
def load_checks_to_execute(checks_file, check_list, provider): # Load checks from groups.json
def parse_groups_from_file(group_list: list, provider: str) -> set:
checks_to_execute = set()
f = open_file(groups_file)
available_groups = parse_json_file(f)
for group in group_list:
if group in available_groups[provider]:
for check_name in available_groups[provider][group]:
checks_to_execute.add(check_name)
else:
logger.error(
f"Group '{group}' was not found for the {provider.upper()} provider"
)
return checks_to_execute
# Generate the list of checks to execute
def load_checks_to_execute(
checks_file: str,
check_list: list,
service_list: list,
group_list: list,
provider: str,
) -> set:
checks_to_execute = set() checks_to_execute = set()
# Handle if there are checks passed using -c/--checks # Handle if there are checks passed using -c/--checks
@@ -42,28 +65,59 @@ def load_checks_to_execute(checks_file, check_list, provider):
# Handle if there are checks passed using -C/--checks-file # Handle if there are checks passed using -C/--checks-file
elif checks_file: elif checks_file:
try: try:
checks_to_execute = parse_checks_from_file(checks_file) checks_to_execute = parse_checks_from_file(checks_file, provider)
except Exception as e: except Exception as e:
logger.error(f"{checks_file}: {e.__class__.__name__}") logger.error(f"{e.__class__.__name__} -- {e}")
# Handle if there are services passed using -s/--services
elif service_list:
# Loaded dynamically from modules within provider/services
for service in service_list:
modules = recover_modules_from_provider(provider, service)
if not modules:
logger.error(f"Service '{service}' was not found for the AWS provider")
else:
for check_module in modules:
# Recover check name and module name from import path
# Format: "providers.{provider}.services.{service}.{check_name}.{check_name}"
check_name = check_module.split(".")[-1]
# If the service is present in the group list passed as parameters
# if service_name in group_list: checks_to_execute.add(check_name)
checks_to_execute.add(check_name)
# Handle if there are groups passed using -g/--groups
elif group_list:
try:
checks_to_execute = parse_groups_from_file(group_list, provider)
except Exception as e:
logger.error(f"{e.__class__.__name__} -- {e}")
# If there are no checks passed as argument # If there are no checks passed as argument
else: else:
# Get all check modules to run with the specific provider try:
modules = recover_modules_from_provider(provider) # Get all check modules to run with the specific provider
for check_module in modules: modules = recover_modules_from_provider(provider)
# Recover check name from import path (last part) except Exception as e:
# Format: "providers.{provider}.services.{service}.{check_name}.{check_name}" logger.error(f"{e.__class__.__name__} -- {e}")
check_name = check_module.split(".")[-1] else:
checks_to_execute.add(check_name) for check_module in modules:
# Recover check name from import path (last part)
# Format: "providers.{provider}.services.{service}.{check_name}.{check_name}"
check_name = check_module.split(".")[-1]
checks_to_execute.add(check_name)
return checks_to_execute return checks_to_execute
def recover_modules_from_provider(provider): def recover_modules_from_provider(provider: str, service: str = None) -> list:
modules = [] modules = []
module_path = f"providers.{provider}.services"
if service:
module_path += f".{service}"
for module_name in pkgutil.walk_packages( for module_name in pkgutil.walk_packages(
importlib.import_module(f"providers.{provider}.services").__path__, importlib.import_module(module_path).__path__,
importlib.import_module(f"providers.{provider}.services").__name__ + ".", importlib.import_module(module_path).__name__ + ".",
): ):
# Format: "providers.{provider}.services.{service}.{check_name}.{check_name}" # Format: "providers.{provider}.services.{service}.{check_name}.{check_name}"
if module_name.name.count(".") == 5: if module_name.name.count(".") == 5:
@@ -78,7 +132,7 @@ def run_check(check):
report(findings) report(findings)
def import_check(check_path): def import_check(check_path: str) -> ModuleType:
lib = importlib.import_module(f"{check_path}") lib = importlib.import_module(f"{check_path}")
return lib return lib
@@ -199,8 +253,9 @@ class Check(ABC):
def __parse_metadata__(self, metadata_file): def __parse_metadata__(self, metadata_file):
# Opening JSON file # Opening JSON file
f = open(metadata_file) f = open_file(metadata_file)
check_metadata = json.load(f) # Parse JSON
check_metadata = parse_json_file(f)
return check_metadata return check_metadata
# Validate metadata # Validate metadata

View File

@@ -1,6 +1,10 @@
import os import os
from lib.check.check import exclude_checks_to_run, parse_checks_from_file from lib.check.check import (
exclude_checks_to_run,
parse_checks_from_file,
parse_groups_from_file,
)
class Test_Check: class Test_Check:
@@ -18,23 +22,29 @@ class Test_Check:
def test_parse_checks_from_file(self): def test_parse_checks_from_file(self):
test_cases = [ test_cases = [
{ {
"input": f"{os.path.dirname(os.path.realpath(__file__))}/fixtures/checklistA.txt", "input": {
"expected": {"check12", "check11", "extra72", "check13"}, "path": f"{os.path.dirname(os.path.realpath(__file__))}/fixtures/checklistA.json",
}, "provider": "aws",
{
"input": f"{os.path.dirname(os.path.realpath(__file__))}/fixtures/checklistB.txt",
"expected": {
"extra72",
"check13",
"check11",
"check12",
"check56",
"check2423",
}, },
}, "expected": {"check11", "check12", "check7777"},
}
] ]
for test in test_cases: for test in test_cases:
assert parse_checks_from_file(test["input"]) == test["expected"] check_file = test["input"]["path"]
provider = test["input"]["provider"]
assert parse_checks_from_file(check_file, provider) == test["expected"]
def test_parse_groups_from_file(self):
test_cases = [
{
"input": {"groups": ["gdpr"], "provider": "aws"},
"expected": {"check11", "check12"},
}
]
for test in test_cases:
provider = test["input"]["provider"]
groups = test["input"]["groups"]
assert parse_groups_from_file(groups, provider) == test["expected"]
def test_exclude_checks_to_run(self): def test_exclude_checks_to_run(self):
test_cases = [ test_cases = [

View File

@@ -0,0 +1,7 @@
{
"aws": [
"check11",
"check12",
"check7777"
]
}

View File

@@ -1,6 +0,0 @@
# You can add a comma seperated list of checks like this:
check11,check12
extra72 # You can also use newlines for each check
check13 # This way allows you to add inline comments
# Both of these can be combined if you have a standard list and want to add
# inline comments for other checks.

View File

@@ -1,11 +0,0 @@
# You can add a comma seperated list of checks like this:
check11,check12
extra72 # You can also use newlines for each check
check13 # This way allows you to add inline comments
# Both of these can be combined if you have a standard list and want to add
# inline comments for other checks.
#
#
#
# check11,check12
check2423,check56

View File

@@ -0,0 +1,8 @@
{
"aws": {
"gdpr": [
"check11",
"check12"
]
}
}

0
lib/utils/__init__.py Normal file
View File

27
lib/utils/utils.py Normal file
View File

@@ -0,0 +1,27 @@
import json
from io import TextIOWrapper
from typing import Any
from lib.logger import logger
def open_file(input_file: str) -> TextIOWrapper:
try:
# First recover the available groups in groups.json
f = open(input_file)
except Exception as e:
logger.critical(f"{input_file}: {e.__class__.__name__}")
quit()
else:
return f
# Parse checks from file
def parse_json_file(input_file: TextIOWrapper) -> Any:
try:
json_file = json.load(input_file)
except Exception as e:
logger.critical(f"{input_file.name}: {e.__class__.__name__}")
quit()
else:
return json_file

View File

@@ -19,10 +19,12 @@ if __name__ == "__main__":
parser.add_argument("provider", choices=["aws"], help="Specify Provider") parser.add_argument("provider", choices=["aws"], help="Specify Provider")
# Arguments to set checks to run # Arguments to set checks to run
# -c can't be used with -C # The following arguments needs to be set exclusivelly
group = parser.add_mutually_exclusive_group() group = parser.add_mutually_exclusive_group()
group.add_argument("-c", "--checks", nargs="+", help="List of checks") group.add_argument("-c", "--checks", nargs="+", help="List of checks")
group.add_argument("-C", "--checks-file", nargs="?", help="List of checks") group.add_argument("-C", "--checks-file", nargs="?", help="List of checks")
group.add_argument("-s", "--services", nargs="+", help="List of services")
group.add_argument("-g", "--groups", nargs="+", help="List of groups")
parser.add_argument("-e", "--excluded-checks", nargs="+", help="Checks to exclude") parser.add_argument("-e", "--excluded-checks", nargs="+", help="Checks to exclude")
parser.add_argument( parser.add_argument(
@@ -79,6 +81,8 @@ if __name__ == "__main__":
provider = args.provider provider = args.provider
checks = args.checks checks = args.checks
excluded_checks = args.excluded_checks excluded_checks = args.excluded_checks
services = args.services
groups = args.groups
checks_file = args.checks_file checks_file = args.checks_file
# Role assumption input options tests # Role assumption input options tests
@@ -124,31 +128,32 @@ if __name__ == "__main__":
# Load checks to execute # Load checks to execute
logger.debug("Loading checks") logger.debug("Loading checks")
checks_to_execute = load_checks_to_execute(checks_file, checks, provider) checks_to_execute = load_checks_to_execute(
checks_file, checks, services, groups, provider
)
# Exclude checks if -e # Exclude checks if -e
if excluded_checks: if excluded_checks:
checks_to_execute = exclude_checks_to_run(checks_to_execute, excluded_checks) checks_to_execute = exclude_checks_to_run(checks_to_execute, excluded_checks)
# Execute checks # Execute checks
for check_name in checks_to_execute: if len(checks_to_execute):
# Recover service from check name for check_name in checks_to_execute:
service = check_name.split("_")[0] # Recover service from check name
try: service = check_name.split("_")[0]
# Import check module try:
check_module_path = ( # Import check module
f"providers.{provider}.services.{service}.{check_name}.{check_name}" check_module_path = (
) f"providers.{provider}.services.{service}.{check_name}.{check_name}"
lib = import_check(check_module_path) )
# Recover functions from check lib = import_check(check_module_path)
check_to_execute = getattr(lib, check_name) # Recover functions from check
c = check_to_execute() check_to_execute = getattr(lib, check_name)
# Run check c = check_to_execute()
run_check(c) # Run check
run_check(c)
# If check does not exists in the provider or is from another provider # If check does not exists in the provider or is from another provider
except ModuleNotFoundError: except ModuleNotFoundError:
logger.error( logger.error(
f"Check '{check_name}' was not found for the {provider.upper()} provider" f"Check '{check_name}' was not found for the {provider.upper()} provider"
) )

View File

@@ -0,0 +1,6 @@
{
"aws": [
"check11",
"check12"
]
}

View File

@@ -1,6 +0,0 @@
# You can add a comma seperated list of checks like this:
check11,check12
extra72 # You can also use newlines for each check
check13 # This way allows you to add inline comments
# Both of these can be combined if you have a standard list and want to add
# inline comments for other checks.