Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(fixer): add Prowler Fixer feature! #3634

Merged
merged 20 commits into from
Apr 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions prowler/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,17 @@
execute_checks,
list_categories,
list_checks_json,
list_fixers,
list_services,
parse_checks_from_folder,
print_categories,
print_checks,
print_compliance_frameworks,
print_compliance_requirements,
print_fixers,
print_services,
remove_custom_checks_module,
run_fixer,
)
from prowler.lib.check.checks_loader import load_checks_to_execute
from prowler.lib.check.compliance import update_checks_metadata_with_compliance
Expand Down Expand Up @@ -94,6 +97,10 @@
print_services(list_services(provider))
sys.exit()

if args.list_fixer:
print_fixers(list_fixers(provider))
sys.exit()

Check warning on line 102 in prowler/__main__.py

View check run for this annotation

Codecov / codecov/patch

prowler/__main__.py#L100-L102

Added lines #L100 - L102 were not covered by tests

# Load checks metadata
logger.debug("Loading checks metadata from .metadata.json files")
bulk_checks_metadata = bulk_load_checks_metadata(provider)
Expand Down Expand Up @@ -210,6 +217,16 @@
"There are no checks to execute. Please, check your input arguments"
)

# Prowler Fixer
if global_provider.output_options.fixer:
print(f"{Style.BRIGHT}\nRunning Prowler Fixer, please wait...{Style.RESET_ALL}")

Check warning on line 222 in prowler/__main__.py

View check run for this annotation

Codecov / codecov/patch

prowler/__main__.py#L221-L222

Added lines #L221 - L222 were not covered by tests
# Check if there are any FAIL findings
if any("FAIL" in finding.status for finding in findings):
run_fixer(findings)

Check warning on line 225 in prowler/__main__.py

View check run for this annotation

Codecov / codecov/patch

prowler/__main__.py#L224-L225

Added lines #L224 - L225 were not covered by tests
else:
print(f"{Style.BRIGHT}{Fore.GREEN}\nNo findings to fix!{Style.RESET_ALL}\n")
sys.exit()

Check warning on line 228 in prowler/__main__.py

View check run for this annotation

Codecov / codecov/patch

prowler/__main__.py#L227-L228

Added lines #L227 - L228 were not covered by tests

# Extract findings stats
stats = extract_findings_statistics(findings)

Expand Down
2 changes: 1 addition & 1 deletion prowler/lib/banner.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"""
print(banner)

if args.verbose:
if args.verbose or getattr(args, "fix", None):

Check warning on line 18 in prowler/lib/banner.py

View check run for this annotation

Codecov / codecov/patch

prowler/lib/banner.py#L18

Added line #L18 was not covered by tests
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this needs to be done force the verbose to be True if the fix is set, I think is more than enough to have one argument to control the outputs.

print(
f"""
Color code for results:
Expand Down
95 changes: 90 additions & 5 deletions prowler/lib/check/check.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@
# Build check path name
check_name = check_info[0]
check_path = check_info[1]
# Ignore fixer files
if check_name.endswith("_fixer"):
continue

Check warning on line 41 in prowler/lib/check/check.py

View check run for this annotation

Codecov / codecov/patch

prowler/lib/check/check.py#L40-L41

Added lines #L40 - L41 were not covered by tests
# Append metadata file extension
metadata_file = f"{check_path}/{check_name}.metadata.json"
# Load metadata
Expand Down Expand Up @@ -203,6 +206,20 @@
return sorted(available_services)


def list_fixers(provider: str) -> set:
available_fixers = set()
checks = recover_checks_from_provider(provider)

Check warning on line 211 in prowler/lib/check/check.py

View check run for this annotation

Codecov / codecov/patch

prowler/lib/check/check.py#L210-L211

Added lines #L210 - L211 were not covered by tests
# Build list of check's metadata files
for check_info in checks:

Check warning on line 213 in prowler/lib/check/check.py

View check run for this annotation

Codecov / codecov/patch

prowler/lib/check/check.py#L213

Added line #L213 was not covered by tests
# Build check path name
check_name = check_info[0]

Check warning on line 215 in prowler/lib/check/check.py

View check run for this annotation

Codecov / codecov/patch

prowler/lib/check/check.py#L215

Added line #L215 was not covered by tests
# Ignore non fixer files
if not check_name.endswith("_fixer"):
continue
available_fixers.add(check_name)
return sorted(available_fixers)

Check warning on line 220 in prowler/lib/check/check.py

View check run for this annotation

Codecov / codecov/patch

prowler/lib/check/check.py#L217-L220

Added lines #L217 - L220 were not covered by tests


def list_categories(bulk_checks_metadata: dict) -> set:
available_categories = set()
for check in bulk_checks_metadata.values():
Expand Down Expand Up @@ -239,6 +256,23 @@
print(message)


def print_fixers(fixers_list: set):
services_num = len(fixers_list)
plural_string = (

Check warning on line 261 in prowler/lib/check/check.py

View check run for this annotation

Codecov / codecov/patch

prowler/lib/check/check.py#L260-L261

Added lines #L260 - L261 were not covered by tests
f"\nThere are {Fore.YELLOW}{services_num}{Style.RESET_ALL} available fixers.\n"
)
singular_string = (

Check warning on line 264 in prowler/lib/check/check.py

View check run for this annotation

Codecov / codecov/patch

prowler/lib/check/check.py#L264

Added line #L264 was not covered by tests
f"\nThere is {Fore.YELLOW}{services_num}{Style.RESET_ALL} available fixer.\n"
)

message = plural_string if services_num > 1 else singular_string

Check warning on line 268 in prowler/lib/check/check.py

View check run for this annotation

Codecov / codecov/patch

prowler/lib/check/check.py#L268

Added line #L268 was not covered by tests

for service in fixers_list:
print(f"- {service}")

Check warning on line 271 in prowler/lib/check/check.py

View check run for this annotation

Codecov / codecov/patch

prowler/lib/check/check.py#L270-L271

Added lines #L270 - L271 were not covered by tests

print(message)

Check warning on line 273 in prowler/lib/check/check.py

View check run for this annotation

Codecov / codecov/patch

prowler/lib/check/check.py#L273

Added line #L273 was not covered by tests


def print_compliance_frameworks(
bulk_compliance_frameworks: dict,
):
Expand Down Expand Up @@ -399,8 +433,16 @@


def run_check(check: Check, output_options) -> list:
"""
Run the check and return the findings
Args:
check (Check): check class
output_options (Any): output options
Returns:
list: list of findings
"""
findings = []
if output_options.verbose:
if output_options.verbose or output_options.fixer:

Check warning on line 445 in prowler/lib/check/check.py

View check run for this annotation

Codecov / codecov/patch

prowler/lib/check/check.py#L445

Added line #L445 was not covered by tests
print(
f"\nCheck ID: {check.CheckID} - {Fore.MAGENTA}{check.ServiceName}{Fore.YELLOW} [{check.Severity}]{Style.RESET_ALL}"
)
Expand All @@ -419,6 +461,45 @@
return findings


def run_fixer(check_findings: list):
"""
Run the fixer for the check if it exists and there are any FAIL findings
Args:
check_findings (list): list of findings
"""
try:

Check warning on line 470 in prowler/lib/check/check.py

View check run for this annotation

Codecov / codecov/patch

prowler/lib/check/check.py#L470

Added line #L470 was not covered by tests
# Map findings to each check
findings_dict = {}
for finding in check_findings:
if finding.check_metadata.CheckID not in findings_dict:
findings_dict[finding.check_metadata.CheckID] = []
findings_dict[finding.check_metadata.CheckID].append(finding)

Check warning on line 476 in prowler/lib/check/check.py

View check run for this annotation

Codecov / codecov/patch

prowler/lib/check/check.py#L472-L476

Added lines #L472 - L476 were not covered by tests

for check, findings in findings_dict.items():

Check warning on line 478 in prowler/lib/check/check.py

View check run for this annotation

Codecov / codecov/patch

prowler/lib/check/check.py#L478

Added line #L478 was not covered by tests
# Check if there are any FAIL findings for the check
if any("FAIL" in finding.status for finding in findings):
try:
check_module_path = f"prowler.providers.{findings[0].check_metadata.Provider}.services.{findings[0].check_metadata.ServiceName}.{check}.{check}_fixer"
lib = import_check(check_module_path)
fixer = getattr(lib, "fixer")
except AttributeError:
logger.error(f"Fixer method not implemented for check {check}")

Check warning on line 486 in prowler/lib/check/check.py

View check run for this annotation

Codecov / codecov/patch

prowler/lib/check/check.py#L480-L486

Added lines #L480 - L486 were not covered by tests
else:
print(

Check warning on line 488 in prowler/lib/check/check.py

View check run for this annotation

Codecov / codecov/patch

prowler/lib/check/check.py#L488

Added line #L488 was not covered by tests
f"\nFixing fails for check {Fore.YELLOW}{check}{Style.RESET_ALL}..."
)
for finding in findings:
if finding.status == "FAIL":
print(

Check warning on line 493 in prowler/lib/check/check.py

View check run for this annotation

Codecov / codecov/patch

prowler/lib/check/check.py#L491-L493

Added lines #L491 - L493 were not covered by tests
f"\t{orange_color}FIXING{Style.RESET_ALL} {finding.region}... {(Fore.GREEN + 'DONE') if fixer(finding.region) else (Fore.RED + 'ERROR')}{Style.RESET_ALL}"
)
print()
except Exception as error:
logger.error(

Check warning on line 498 in prowler/lib/check/check.py

View check run for this annotation

Codecov / codecov/patch

prowler/lib/check/check.py#L496-L498

Added lines #L496 - L498 were not covered by tests
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)


def execute_checks(
checks_to_execute: list,
global_provider: Any,
Expand Down Expand Up @@ -569,14 +650,18 @@
lib = import_check(check_module_path)
# Recover functions from check
check_to_execute = getattr(lib, check_name)
c = check_to_execute()
check_class = check_to_execute()

Check warning on line 653 in prowler/lib/check/check.py

View check run for this annotation

Codecov / codecov/patch

prowler/lib/check/check.py#L653

Added line #L653 was not covered by tests

# Update check metadata to reflect that in the outputs
if custom_checks_metadata and custom_checks_metadata["Checks"].get(c.CheckID):
c = update_check_metadata(c, custom_checks_metadata["Checks"][c.CheckID])
if custom_checks_metadata and custom_checks_metadata["Checks"].get(

Check warning on line 656 in prowler/lib/check/check.py

View check run for this annotation

Codecov / codecov/patch

prowler/lib/check/check.py#L656

Added line #L656 was not covered by tests
check_class.CheckID
):
check_class = update_check_metadata(

Check warning on line 659 in prowler/lib/check/check.py

View check run for this annotation

Codecov / codecov/patch

prowler/lib/check/check.py#L659

Added line #L659 was not covered by tests
check_class, custom_checks_metadata["Checks"][check_class.CheckID]
)

# Run check
check_findings = run_check(c, global_provider.output_options)
check_findings = run_check(check_class, global_provider.output_options)

Check warning on line 664 in prowler/lib/check/check.py

View check run for this annotation

Codecov / codecov/patch

prowler/lib/check/check.py#L664

Added line #L664 was not covered by tests

# Update Audit Status
services_executed.add(service)
Expand Down
2 changes: 1 addition & 1 deletion prowler/lib/check/checks_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ def load_checks_to_execute(

# Only execute threat detection checks if threat-detection category is set
if "threat-detection" not in categories:
for threat_detection_check in check_categories["threat-detection"]:
for threat_detection_check in check_categories.get("threat-detection", []):
checks_to_execute.discard(threat_detection_check)

# Check Aliases
Expand Down
7 changes: 7 additions & 0 deletions prowler/lib/cli/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,7 @@ def __init_list_checks_parser__(self):
)
list_group.add_argument(
"--list-compliance",
"--list-compliances",
action="store_true",
help="List all available compliance frameworks",
)
Expand All @@ -305,6 +306,12 @@ def __init_list_checks_parser__(self):
action="store_true",
help="List the available check's categories",
)
list_group.add_argument(
"--list-fixer",
"--list-fixers",
action="store_true",
help="List fixers available for the provider",
)

def __init_mutelist_parser__(self):
mutelist_subparser = self.common_providers_parser.add_argument_group(
Expand Down
12 changes: 8 additions & 4 deletions prowler/lib/outputs/outputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from prowler.lib.outputs.utils import unroll_dict


def stdout_report(finding, color, verbose, status):
def stdout_report(finding, color, verbose, status, fix):
if finding.check_metadata.Provider == "aws":
details = finding.region
if finding.check_metadata.Provider == "azure":
Expand All @@ -33,7 +33,7 @@
if finding.check_metadata.Provider == "kubernetes":
details = finding.namespace.lower()

if verbose and (not status or finding.status in status):
if (verbose or fix) and (not status or finding.status in status):

Check warning on line 36 in prowler/lib/outputs/outputs.py

View check run for this annotation

Codecov / codecov/patch

prowler/lib/outputs/outputs.py#L36

Added line #L36 was not covered by tests
if finding.muted:
print(
f"\t{color}MUTED ({finding.status}){Style.RESET_ALL} {details}: {finding.status_extended}"
Expand All @@ -57,7 +57,7 @@
check_findings.sort(key=lambda x: x.subscription)

# Generate the required output files
if output_options.output_modes:
if output_options.output_modes and not output_options.fixer:

Check warning on line 60 in prowler/lib/outputs/outputs.py

View check run for this annotation

Codecov / codecov/patch

prowler/lib/outputs/outputs.py#L60

Added line #L60 was not covered by tests
# We have to create the required output files
file_descriptors = fill_file_descriptors(
output_options.output_modes,
Expand All @@ -70,7 +70,11 @@
# Print findings by stdout
color = set_report_color(finding.status, finding.muted)
stdout_report(
finding, color, output_options.verbose, output_options.status
finding,
color,
output_options.verbose,
output_options.status,
output_options.fixer,
)

if file_descriptors:
Expand Down
7 changes: 4 additions & 3 deletions prowler/providers/aws/aws_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -496,8 +496,7 @@
f"{Style.BRIGHT}AWS Regions: {Style.RESET_ALL}{Fore.YELLOW}{regions}{Style.RESET_ALL}",
f"{Style.BRIGHT}AWS Account: {Style.RESET_ALL}{Fore.YELLOW}{self._identity.account}{Style.RESET_ALL}",
f"{Style.BRIGHT}User Id: {Style.RESET_ALL}{Fore.YELLOW}{self._identity.user_id}{Style.RESET_ALL}",
f"{Style.BRIGHT}Caller Identity ARN: {Style.RESET_ALL}",
f"{Fore.YELLOW}{self._identity.identity_arn}{Style.RESET_ALL}",
f"{Style.BRIGHT}Caller Identity ARN: {Style.RESET_ALL}{Fore.YELLOW}{self._identity.identity_arn}{Style.RESET_ALL}",
]
# If -A is set, print Assumed Role ARN
if (
Expand All @@ -507,7 +506,9 @@
report_lines.append(
f"Assumed Role ARN: {Fore.YELLOW}[{self._assumed_role.info.role_arn.arn}]{Style.RESET_ALL}"
)
report_title = f"{Style.BRIGHT}Prowler is using the AWS credentials below:{Style.RESET_ALL}"
report_title = (

Check warning on line 509 in prowler/providers/aws/aws_provider.py

View check run for this annotation

Codecov / codecov/patch

prowler/providers/aws/aws_provider.py#L509

Added line #L509 was not covered by tests
f"{Style.BRIGHT}Using the AWS credentials below:{Style.RESET_ALL}"
)
print_boxes(report_lines, report_title)

def generate_regional_clients(
Expand Down
8 changes: 8 additions & 0 deletions prowler/providers/aws/lib/arguments/arguments.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,14 @@ def init_parser(self):
help="Scan unused services",
)

# Prowler Fixer
prowler_fixer_subparser = aws_parser.add_argument_group("Prowler Fixer")
prowler_fixer_subparser.add_argument(
"--fixer",
action="store_true",
help="Fix the failed findings that can be fixed by Prowler",
)


def validate_session_duration(duration):
"""validate_session_duration validates that the AWS STS Assume Role Session Duration is between 900 and 43200 seconds."""
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from prowler.lib.logger import logger
from prowler.providers.aws.services.ec2.ec2_client import ec2_client


def fixer(region):
"""
Enable EBS encryption by default in a region.
Requires the ec2:EnableEbsEncryptionByDefault permission:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "ec2:EnableEbsEncryptionByDefault",
"Resource": "*"
}
]
}
Args:
region (str): AWS region
Returns:
bool: True if EBS encryption by default is enabled, False otherwise
"""
try:
regional_client = ec2_client.regional_clients[region]
return regional_client.enable_ebs_encryption_by_default()[
"EbsEncryptionByDefault"
]
except Exception as error:
logger.error(
f"{region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
return False
4 changes: 3 additions & 1 deletion prowler/providers/azure/azure_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,9 @@
f"{Style.BRIGHT}Azure Subscriptions:{Style.RESET_ALL} {Fore.YELLOW}{printed_subscriptions}{Style.RESET_ALL}",
f"{Style.BRIGHT}Azure Identity Type:{Style.RESET_ALL} {Fore.YELLOW}{self._identity.identity_type}{Style.RESET_ALL} {Style.BRIGHT}Azure Identity ID:{Style.RESET_ALL} {Fore.YELLOW}{self._identity.identity_id}{Style.RESET_ALL}",
]
report_title = f"{Style.BRIGHT}Prowler is using the Azure credentials below:{Style.RESET_ALL}"
report_title = (

Check warning on line 185 in prowler/providers/azure/azure_provider.py

View check run for this annotation

Codecov / codecov/patch

prowler/providers/azure/azure_provider.py#L185

Added line #L185 was not covered by tests
f"{Style.BRIGHT}Using the Azure credentials below:{Style.RESET_ALL}"
)
print_boxes(report_lines, report_title)

# TODO: setup_session or setup_credentials?
Expand Down
3 changes: 2 additions & 1 deletion prowler/providers/common/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,14 @@ def __init__(self, arguments, bulk_checks_metadata):
self.only_logs = arguments.only_logs
self.unix_timestamp = arguments.unix_timestamp
self.shodan_api_key = arguments.shodan
self.fixer = getattr(arguments, "fixer", None)

# Shodan API Key
if arguments.shodan:
update_provider_config("shodan_api_key", arguments.shodan)

# Check output directory, if it is not created -> create it
if arguments.output_directory:
if arguments.output_directory and not self.fixer:
if not isdir(arguments.output_directory):
if arguments.output_formats:
makedirs(arguments.output_directory, exist_ok=True)
Expand Down
4 changes: 3 additions & 1 deletion prowler/providers/gcp/gcp_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,9 @@
f"{Style.BRIGHT}GCP Account:{Style.RESET_ALL} {Fore.YELLOW}{self.identity.profile}{Style.RESET_ALL}",
f"{Style.BRIGHT}GCP Project IDs:{Style.RESET_ALL} {Fore.YELLOW}{', '.join(self.project_ids)}{Style.RESET_ALL}",
]
report_title = f"{Style.BRIGHT}Prowler is using the GCP credentials below:{Style.RESET_ALL}"
report_title = (

Check warning on line 175 in prowler/providers/gcp/gcp_provider.py

View check run for this annotation

Codecov / codecov/patch

prowler/providers/gcp/gcp_provider.py#L175

Added line #L175 was not covered by tests
f"{Style.BRIGHT}Using the GCP credentials below:{Style.RESET_ALL}"
)
print_boxes(report_lines, report_title)

def get_projects(self) -> dict[str, GCPProject]:
Expand Down
4 changes: 3 additions & 1 deletion prowler/providers/kubernetes/kubernetes_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,5 +279,7 @@
f"{Style.BRIGHT}Namespaces:{Style.RESET_ALL} {Fore.YELLOW}{', '.join(self.namespaces)}{Style.RESET_ALL}",
f"{Style.BRIGHT}Roles:{Style.RESET_ALL} {Fore.YELLOW}{roles_str}{Style.RESET_ALL}",
]
report_title = f"{Style.BRIGHT}Prowler is using the Kubernetes credentials below:{Style.RESET_ALL}"
report_title = (

Check warning on line 282 in prowler/providers/kubernetes/kubernetes_provider.py

View check run for this annotation

Codecov / codecov/patch

prowler/providers/kubernetes/kubernetes_provider.py#L282

Added line #L282 was not covered by tests
f"{Style.BRIGHT}Using the Kubernetes credentials below:{Style.RESET_ALL}"
)
print_boxes(report_lines, report_title)
Loading