feat(display): add progress bar and summary table (#1512)

Co-authored-by: sergargar <sergio@verica.io>
This commit is contained in:
Sergio Garcia
2022-11-22 11:18:43 +01:00
committed by GitHub
parent af1d85ae75
commit 9204142eaf
16 changed files with 320 additions and 106 deletions

View File

@@ -1,14 +1,14 @@
from colorama import Fore, Style
from config.config import prowler_version, timestamp
from config.config import banner_color, orange_color, prowler_version, timestamp
def print_version():
print(f"Prowler {prowler_version}")
def print_banner():
banner = f"""{Fore.CYAN} _
def print_banner(args):
banner = f"""{banner_color} _
_ __ _ __ _____ _| | ___ _ __
| '_ \| '__/ _ \ \ /\ / / |/ _ \ '__|
| |_) | | | (_) \ V V /| | __/ |
@@ -16,11 +16,16 @@ def print_banner():
|_|{Fore.BLUE} the handy cloud security tool
{Fore.YELLOW}Date: {timestamp.strftime("%Y-%m-%d %H:%M:%S")}{Style.RESET_ALL}
Color code for results:
- {Fore.YELLOW}INFO (Information){Style.RESET_ALL}
- {Fore.GREEN}PASS (Recommended value){Style.RESET_ALL}
- {Fore.YELLOW}WARNING (Ignored by allowlist){Style.RESET_ALL}
- {Fore.RED}FAIL (Fix required){Style.RESET_ALL}
"""
print(banner)
if args.verbose or args.quiet:
print(
f"""
Color code for results:
- {Fore.YELLOW}INFO (Information){Style.RESET_ALL}
- {Fore.GREEN}PASS (Recommended value){Style.RESET_ALL}
- {orange_color}WARNING (Ignored by allowlist){Style.RESET_ALL}
- {Fore.RED}FAIL (Fix required){Style.RESET_ALL}
"""
)

View File

@@ -3,13 +3,15 @@ from pkgutil import walk_packages
from types import ModuleType
from typing import Any
from alive_progress import alive_bar
from colorama import Fore, Style
from config.config import groups_file
from lib.check.models import Output_From_Options, load_check_metadata
from config.config import groups_file, orange_color
from lib.check.models import Check, Output_From_Options, load_check_metadata
from lib.logger import logger
from lib.outputs.outputs import report
from lib.utils.utils import open_file, parse_json_file
from providers.aws.lib.audit_info.models import AWS_Audit_Info
# Load all checks metadata
@@ -189,6 +191,7 @@ def set_output_options(
security_hub_enabled: bool,
output_filename: str,
allowlist_file: str,
verbose: bool,
):
global output_options
output_options = Output_From_Options(
@@ -198,15 +201,18 @@ def set_output_options(
security_hub_enabled=security_hub_enabled,
output_filename=output_filename,
allowlist_file=allowlist_file,
verbose=verbose,
# set input options here
)
return output_options
def run_check(check, audit_info, output_options):
print(
f"\nCheck ID: {check.checkID} - {Fore.MAGENTA}{check.serviceName}{Fore.YELLOW} [{check.severity}]{Style.RESET_ALL}"
)
def run_check(check: Check, output_options: Output_From_Options) -> list:
findings = []
if output_options.verbose or output_options.is_quiet:
print(
f"\nCheck ID: {check.checkID} - {Fore.MAGENTA}{check.serviceName}{Fore.YELLOW} [{check.severity}]{Style.RESET_ALL}"
)
logger.debug(f"Executing check: {check.checkID}")
try:
findings = check.execute()
@@ -215,7 +221,54 @@ def run_check(check, audit_info, output_options):
logger.error(
f"{check.checkID} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
else:
report(findings, output_options, audit_info)
finally:
pass
return findings
def execute_checks(
checks_to_execute: list,
provider: str,
audit_info: AWS_Audit_Info,
audit_output_options: Output_From_Options,
) -> list:
all_findings = []
print(
f"{Style.BRIGHT}Executing {len(checks_to_execute)} checks, please wait...{Style.RESET_ALL}\n"
)
with alive_bar(
total=len(checks_to_execute),
ctrl_c=False,
bar="blocks",
spinner="classic",
stats=False,
enrich_print=False,
) as bar:
for check_name in checks_to_execute:
# Recover service from check name
service = check_name.split("_")[0]
bar.title = f"-> Scanning {orange_color}{service}{Style.RESET_ALL} service"
try:
# Import check module
check_module_path = (
f"providers.{provider}.services.{service}.{check_name}.{check_name}"
)
lib = import_check(check_module_path)
# Recover functions from check
check_to_execute = getattr(lib, check_name)
c = check_to_execute()
# Run check
check_findings = run_check(c, audit_output_options)
all_findings.extend(check_findings)
report(check_findings, audit_output_options, audit_info)
bar()
# If check does not exists in the provider or is from another provider
except ModuleNotFoundError:
logger.error(
f"Check '{check_name}' was not found for the {provider.upper()} provider"
)
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
return all_findings

View File

@@ -16,6 +16,7 @@ class Output_From_Options:
security_hub_enabled: bool
output_filename: str
allowlist_file: str
verbose: str
# Testing Pending

View File

@@ -4,11 +4,13 @@ import sys
from csv import DictWriter
from colorama import Fore, Style
from tabulate import tabulate
from config.config import (
csv_file_suffix,
json_asff_file_suffix,
json_file_suffix,
orange_color,
prowler_version,
timestamp_iso,
timestamp_utc,
@@ -25,6 +27,7 @@ from lib.outputs.models import (
)
from lib.utils.utils import file_exists, hash_sha512, open_file
from providers.aws.lib.allowlist.allowlist import is_allowlisted
from providers.aws.lib.audit_info.models import AWS_Audit_Info
from providers.aws.lib.security_hub.security_hub import send_to_security_hub
@@ -62,7 +65,7 @@ def report(check_findings, output_options, audit_info):
print(
f"\t{color}{finding.status}{Style.RESET_ALL} {finding.region}: {finding.status_extended}"
)
elif not output_options.is_quiet:
elif not output_options.is_quiet and output_options.verbose:
print(
f"\t{color}{finding.status}{Style.RESET_ALL} {finding.region}: {finding.status_extended}"
)
@@ -103,8 +106,8 @@ def report(check_findings, output_options, audit_info):
finding.region, finding_output, audit_info.audit_session
)
else: # No service resources in the whole account
color = set_report_color("WARNING")
if not output_options.is_quiet:
color = set_report_color("INFO")
if not output_options.is_quiet and output_options.verbose:
print(f"\t{color}INFO{Style.RESET_ALL} There are no resources")
if file_descriptors:
@@ -180,6 +183,8 @@ def set_report_color(status):
elif status == "ERROR":
color = Fore.BLACK
elif status == "WARNING":
color = orange_color
elif status == "INFO":
color = Fore.YELLOW
else:
raise Exception("Invalid Report Status. Must be PASS, FAIL, ERROR or WARNING")
@@ -289,3 +294,111 @@ def send_to_s3_bucket(
except Exception as error:
logger.critical(f"{error.__class__.__name__} -- {error}")
sys.exit()
def display_summary_table(
findings: list,
audit_info: AWS_Audit_Info,
output_filename: str,
output_directory: str,
):
try:
if findings:
current = {
"Service": "",
"Provider": "",
"Critical": 0,
"High": 0,
"Medium": 0,
"Low": 0,
}
findings_table = {
"Provider": [],
"Service": [],
"Status": [],
"Critical": [],
"High": [],
"Medium": [],
"Low": [],
}
pass_count = fail_count = 0
for finding in findings:
# If new service and not first, add previous row
if (
current["Service"] != finding.check_metadata.ServiceName
and current["Service"]
):
add_service_to_table(findings_table, current)
current["Critical"] = current["High"] = current["Medium"] = current[
"Low"
] = 0
current["Service"] = finding.check_metadata.ServiceName
current["Provider"] = finding.check_metadata.Provider
if finding.status == "PASS":
pass_count += 1
elif finding.status == "FAIL":
fail_count += 1
if finding.check_metadata.Severity == "critical":
current["Critical"] += 1
elif finding.check_metadata.Severity == "high":
current["High"] += 1
elif finding.check_metadata.Severity == "medium":
current["Medium"] += 1
elif finding.check_metadata.Severity == "low":
current["Low"] += 1
# Add final service
add_service_to_table(findings_table, current)
print("\nOverview Results:")
overview_table = [
[
f"{Fore.RED}{round(fail_count/len(findings)*100, 2)}% ({fail_count}) Failed{Style.RESET_ALL}",
f"{Fore.GREEN}{round(pass_count/len(findings)*100, 2)}% ({pass_count}) Passed{Style.RESET_ALL}",
]
]
print(tabulate(overview_table, tablefmt="rounded_grid"))
print(
f"\nAccount {Fore.YELLOW}{audit_info.audited_account}{Style.RESET_ALL} Scan Results (severity columns are for fails only):"
)
print(tabulate(findings_table, headers="keys", tablefmt="rounded_grid"))
print(
f"{Style.BRIGHT}* You only see here those services that contains resources.{Style.RESET_ALL}"
)
print("\nDetailed results are in:")
print(f" - CSV: {output_directory}/{output_filename}.csv")
print(f" - JSON: {output_directory}/{output_filename}.json\n")
except Exception as error:
logger.critical(
f"{error.__class__.__name__}:{error.__traceback__.tb_lineno} -- {error}"
)
sys.exit()
def add_service_to_table(findings_table, current):
if (
current["Critical"] > 0
or current["High"] > 0
or current["Medium"] > 0
or current["Low"] > 0
):
current["Status"] = f"{Fore.RED}FAIL{Style.RESET_ALL}"
else:
current["Status"] = f"{Fore.GREEN}PASS{Style.RESET_ALL}"
findings_table["Provider"].append(current["Provider"])
findings_table["Service"].append(current["Service"])
findings_table["Status"].append(current["Status"])
findings_table["Critical"].append(
f"{Fore.LIGHTRED_EX}{current['Critical']}{Style.RESET_ALL}"
)
findings_table["High"].append(f"{Fore.RED}{current['High']}{Style.RESET_ALL}")
findings_table["Medium"].append(
f"{Fore.YELLOW}{current['Medium']}{Style.RESET_ALL}"
)
findings_table["Low"].append(f"{Fore.BLUE}{current['Low']}{Style.RESET_ALL}")

View File

@@ -13,6 +13,7 @@ from config.config import (
prowler_version,
timestamp_iso,
timestamp_utc,
orange_color,
)
from lib.check.models import Check_Report, load_check_metadata
from lib.outputs.models import (
@@ -109,7 +110,7 @@ class Test_Outputs:
def test_set_report_color(self):
test_status = ["PASS", "FAIL", "ERROR", "WARNING"]
test_colors = [Fore.GREEN, Fore.RED, Fore.BLACK, Fore.YELLOW]
test_colors = [Fore.GREEN, Fore.RED, Fore.BLACK, orange_color]
for status in test_status:
assert set_report_color(status) in test_colors