mirror of
https://github.com/ghndrx/prowler.git
synced 2026-02-10 06:45:08 +00:00
feat(groups): Launch specific checks from groups and services (#1204)
This commit is contained in:
@@ -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
0
config/__init__.py
Normal 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
8
groups.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"aws": {
|
||||||
|
"gdpr": [
|
||||||
|
"check11",
|
||||||
|
"check12"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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():
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 = [
|
||||||
|
|||||||
7
lib/check/fixtures/checklistA.json
Normal file
7
lib/check/fixtures/checklistA.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"aws": [
|
||||||
|
"check11",
|
||||||
|
"check12",
|
||||||
|
"check7777"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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.
|
|
||||||
@@ -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
|
|
||||||
8
lib/check/fixtures/groupsA.json
Normal file
8
lib/check/fixtures/groupsA.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"aws": {
|
||||||
|
"gdpr": [
|
||||||
|
"check11",
|
||||||
|
"check12"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
0
lib/utils/__init__.py
Normal file
0
lib/utils/__init__.py
Normal file
27
lib/utils/utils.py
Normal file
27
lib/utils/utils.py
Normal 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
|
||||||
51
prowler.py
51
prowler.py
@@ -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"
|
||||||
)
|
)
|
||||||
|
|||||||
6
util/checklist_example.json
Normal file
6
util/checklist_example.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"aws": [
|
||||||
|
"check11",
|
||||||
|
"check12"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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.
|
|
||||||
Reference in New Issue
Block a user