From 11652838e2ac9c804c6f78b78701fb88c58c4b00 Mon Sep 17 00:00:00 2001 From: Nacho Rivera <59198746+n4ch04@users.noreply.github.com> Date: Mon, 4 Jul 2022 10:30:47 +0200 Subject: [PATCH] feat(outputS): Output generation format CSV (#1230) * chore(csv): first version csv output * chore(pytest): added pytest dependency * chore(outputs): organizations demo * chore(compliance): Added new dataclass for each compliance framework * fix(test org values): deleted test values in orgs instantiation * fix(csv): formatted to match output format * fix(csv output): Reformulation of check report and minor changes * fix(minor issues): Fix various issues coming from PR comments * fix(csv): Renamed csv output data model * fix(output dir): create default if not present * fix(typo): remove s * fix(oldcode) * fix(typo) * fix(output): Only send to csv when -M is passed Co-authored-by: sergargar Co-authored-by: Pepe Fagoaga --- Pipfile | 1 + Pipfile.lock | 77 ++++++- config/config.py | 10 +- lib/banner.py | 2 +- lib/check/check.py | 16 +- lib/check/models.py | 48 +++-- lib/outputs.py | 30 --- lib/outputs/models.py | 189 ++++++++++++++++++ lib/outputs/outputs.py | 119 +++++++++++ lib/utils/utils.py | 20 +- providers/aws/aws_provider.py | 4 + .../ec2_ebs_snapshots_encrypted.metadata.json | 11 + .../ec2_ebs_snapshots_encrypted.py | 16 +- .../iam_disable_30_days_credentials.py | 17 +- .../iam_disable_90_days_credentials.py | 21 +- prowler | 41 +++- 16 files changed, 532 insertions(+), 90 deletions(-) delete mode 100644 lib/outputs.py create mode 100644 lib/outputs/models.py create mode 100644 lib/outputs/outputs.py diff --git a/Pipfile b/Pipfile index ef2fd2cc..46eb553a 100644 --- a/Pipfile +++ b/Pipfile @@ -9,6 +9,7 @@ boto3 = "1.24.8" arnparse = "0.0.2" botocore = "1.27.8" pydantic = "1.9.1" +pytest = "7.1.2" [dev-packages] diff --git a/Pipfile.lock b/Pipfile.lock index 086252a8..ae43d0d4 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "b532ef32ebcb28be5438c1ef9c717aa6792cfd5098ad81a9ed35520a245bb8f2" + "sha256": "f675454f65aef11134860cda6cb84cddfecfb17827ce6efbe05ce7ae8efbb926" }, "pipfile-spec": 6, "requires": { @@ -24,21 +24,29 @@ "index": "pypi", "version": "==0.0.2" }, + "attrs": { + "hashes": [ + "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4", + "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==21.4.0" + }, "boto3": { "hashes": [ - "sha256:490f5e88f5551b33ae3019a37412158b76426d63d1fb910968ade9b6a024e5fe", - "sha256:e284705da36faa668c715ae1f74ebbff4320dbfbe3a733df3a8ab076d1ed1226" + "sha256:551e902b70ccf9f6a58e28bb409718a0403b021b17ff6d63ab0b9af5a122386e", + "sha256:abe5b44010e3f50c5e0243aa4fc2338f10e5a868413faa0d6ae79131d6b507b8" ], "index": "pypi", - "version": "==1.24.14" + "version": "==1.24.21" }, "botocore": { "hashes": [ - "sha256:bb56fa77b8fa1ec367c2e16dee62d60000451aac5140dcce3ebddc167fd5c593", - "sha256:df1e9b208ff93daac7c645b0b04fb6dccd7f20262eae24d87941727025cbeece" + "sha256:3bafa8e773d207c0ce02c63790a8820562e22d2e892abaf1eb90c343e995218a", + "sha256:b685ffc0ac170bf7de5fde931504eccd939f8545a9c9d10259245ca4c91101e5" ], "index": "pypi", - "version": "==1.27.14" + "version": "==1.27.21" }, "colorama": { "hashes": [ @@ -48,6 +56,13 @@ "index": "pypi", "version": "==0.4.5" }, + "iniconfig": { + "hashes": [ + "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", + "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32" + ], + "version": "==1.1.1" + }, "jmespath": { "hashes": [ "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", @@ -56,6 +71,30 @@ "markers": "python_version >= '3.7'", "version": "==1.0.1" }, + "packaging": { + "hashes": [ + "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", + "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" + ], + "markers": "python_version >= '3.6'", + "version": "==21.3" + }, + "pluggy": { + "hashes": [ + "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", + "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" + ], + "markers": "python_version >= '3.6'", + "version": "==1.0.0" + }, + "py": { + "hashes": [ + "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", + "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==1.11.0" + }, "pydantic": { "hashes": [ "sha256:02eefd7087268b711a3ff4db528e9916ac9aa18616da7bca69c1871d0b7a091f", @@ -97,6 +136,22 @@ "index": "pypi", "version": "==1.9.1" }, + "pyparsing": { + "hashes": [ + "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb", + "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc" + ], + "markers": "python_full_version >= '3.6.8'", + "version": "==3.0.9" + }, + "pytest": { + "hashes": [ + "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c", + "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45" + ], + "index": "pypi", + "version": "==7.1.2" + }, "python-dateutil": { "hashes": [ "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", @@ -121,6 +176,14 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.16.0" }, + "tomli": { + "hashes": [ + "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", + "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" + ], + "markers": "python_version >= '3.7'", + "version": "==2.0.1" + }, "typing-extensions": { "hashes": [ "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708", diff --git a/config/config.py b/config/config.py index 154c3d48..359f51b4 100644 --- a/config/config.py +++ b/config/config.py @@ -1,10 +1,16 @@ from datetime import datetime +from os import getcwd -timestamp = datetime.today().strftime("%Y-%m-%d %H:%M:%S") +timestamp = datetime.today() prowler_version = "3.0-alfa" # Groups groups_file = "groups.json" # AWS services-regions matrix json -aws_services_json_file = "providers/aws/aws_regions_by_service.json" +aws_services_json_file = "providers/aws/aws_regions_services.json" + +default_output_directory = getcwd() + "/output" + +csv_file_suffix = timestamp.strftime("%Y%m%d%H%M%S") + ".csv" + diff --git a/lib/banner.py b/lib/banner.py index a6d84d89..8c1cce16 100644 --- a/lib/banner.py +++ b/lib/banner.py @@ -14,6 +14,6 @@ def print_banner(): | |_) | | | (_) \ V V /| | __/ | | .__/|_| \___/ \_/\_/ |_|\___|_|v{prowler_version} |_|{Fore.BLUE} the handy cloud security tool -{Fore.YELLOW} Date: {timestamp}{Style.RESET_ALL} +{Fore.YELLOW} Date: {timestamp.strftime("%Y-%m-%d %H:%M:%S")}{Style.RESET_ALL} """ print(banner) diff --git a/lib/check/check.py b/lib/check/check.py index 7bda1ffc..bf3188a2 100644 --- a/lib/check/check.py +++ b/lib/check/check.py @@ -9,7 +9,7 @@ from colorama import Fore, Style from config.config import groups_file from lib.check.models import Output_From_Options, load_check_metadata from lib.logger import logger -from lib.outputs import report +from lib.outputs.outputs import get_orgs_info, report from lib.utils.utils import open_file, parse_json_file @@ -175,22 +175,28 @@ def import_check(check_path: str) -> ModuleType: return lib -def set_output_options(quiet): +def set_output_options(quiet: bool, output_modes: list, input_output_directory: str): global output_options output_options = Output_From_Options( - is_quiet=quiet + is_quiet=quiet, + output_modes=output_modes, + output_directory=input_output_directory # set input options here ) return output_options -def run_check(check): +def run_check(check, audit_info, output_options): print( f"\nCheck Name: {check.checkName} - {Fore.MAGENTA}{check.serviceName}{Fore.YELLOW} [{check.severity}]{Style.RESET_ALL}" ) logger.debug(f"Executing check: {check.checkName}") findings = check.execute() - report(findings, output_options) + + # Call to get orgs, need to check if input option is passed in output options + # right now it is not checked and is called straight to generate the fields to be passed to the csv + organizations_info = get_orgs_info() + report(findings, output_options, audit_info, organizations_info) def import_check(check_path: str) -> ModuleType: diff --git a/lib/check/models.py b/lib/check/models.py index 4f3e778c..92c74202 100644 --- a/lib/check/models.py +++ b/lib/check/models.py @@ -8,21 +8,11 @@ from pydantic import BaseModel, ValidationError from lib.logger import logger -@dataclass -class Check_Report: - status: str - region: str - result_extended: str - - def __init__(self): - self.status = "" - self.region = "" - self.result_extended = "" - - @dataclass class Output_From_Options: is_quiet: bool + output_modes: list + output_directory: str # Testing Pending @@ -174,6 +164,40 @@ class Check(ABC): def compliance(self): return self.__Compliance__ + @property + def metadata(self): + return self.__check_metadata__ + @abstractmethod def execute(self): pass + + +@dataclass +class Check_Report: + status: str + region: str + status_extended: str + check_metadata: dict + status_extended: str + resource_id: str + resource_details: str + resource_tags: str + resource_arn: str + + def __init__(self, metadata): + self.check_metadata = metadata + self.status_extended = "" + self.resource_details = "" + self.resource_tags = [] + self.resource_id = "" + self.resource_arn = "" + + +@dataclass +class Organizations_Info: + account_details_email: str + account_details_name: str + account_details_arn: str + account_details_org: str + account_details_tags: str diff --git a/lib/outputs.py b/lib/outputs.py deleted file mode 100644 index ae52376c..00000000 --- a/lib/outputs.py +++ /dev/null @@ -1,30 +0,0 @@ -from colorama import Fore, Style - - -def report(check_findings, output_options): - check_findings.sort(key=lambda x: x.region) - for finding in check_findings: - color = set_report_color(finding.status) - if output_options.is_quiet and "FAIL" in finding.status: - print( - f"{color}{finding.status}{Style.RESET_ALL} {finding.region}: {finding.result_extended}" - ) - elif not output_options.is_quiet: - print( - f"{color}{finding.status}{Style.RESET_ALL} {finding.region}: {finding.result_extended}" - ) - - -def set_report_color(status): - color = "" - if status == "PASS": - color = Fore.GREEN - elif status == "FAIL": - color = Fore.RED - elif status == "ERROR": - color = Fore.BLACK - elif status == "WARNING": - color = Fore.YELLOW - else: - raise Exception("Invalid Report Status. Must be PASS, FAIL, ERROR or WARNING") - return color diff --git a/lib/outputs/models.py b/lib/outputs/models.py new file mode 100644 index 00000000..c676565d --- /dev/null +++ b/lib/outputs/models.py @@ -0,0 +1,189 @@ +from dataclasses import asdict, dataclass + +from config.config import timestamp +from lib.check.models import Check_Report, Organizations_Info + + +@dataclass +class Compliance_Framework: + Framework: str + Version: str + Group: list + Control: list + + +@dataclass +class Check_Output_CSV: + assessment_start_time: str + finding_unique_id: str + provider: str + profile: str + account_id: int + account_name: str + account_email: str + account_arn: str + account_org: str + account_tags: str + region: str + check_id: str + check_name: str + check_title: str + check_type: str + status: str + status_extended: str + service_name: str + subservice_name: str + severity: str + resource_id: str + resource_arn: str + resource_type: str + resource_details: str + resource_tags: list + description: dict + risk: list + related_url: list + remediation_recommendation_text: str + remediation_recommendation_url: list + remediation_recommendation_code_nativeiac: str + remediation_recommendation_code_terraform: str + remediation_recommendation_code_cli: str + remediation_recommendation_code_other: str + categories: str + depends_on: str + related_to: str + notes: str + compliance: str + + def get_csv_header(self): + csv_header = [] + for key in asdict(self): + csv_header = csv_header.append(key) + return csv_header + + def __init__( + self, + account: str, + profile: str, + report: Check_Report, + organizations: Organizations_Info, + ): + self.assessment_start_time = timestamp.isoformat() + self.finding_unique_id = "" + self.provider = report.check_metadata.Provider + self.profile = profile + self.account_id = account + self.account_name = organizations.account_details_name + self.account_email = organizations.account_details_email + self.account_arn = organizations.account_details_arn + self.account_org = organizations.account_details_org + self.account_tags = organizations.account_details_tags + self.region = report.region + self.check_id = report.check_metadata.CheckID + self.check_name = report.check_metadata.CheckName + self.check_title = report.check_metadata.CheckTitle + self.check_type = report.check_metadata.CheckType + self.status = report.status + self.status_extended = report.status_extended + self.service_name = report.check_metadata.ServiceName + self.subservice_name = report.check_metadata.SubServiceName + self.severity = report.check_metadata.Severity + self.resource_id = report.resource_id + self.resource_arn = report.resource_arn + self.resource_type = report.check_metadata.ResourceType + self.resource_details = report.resource_details + self.resource_tags = report.resource_tags + self.description = report.check_metadata.Description + self.risk = report.check_metadata.Risk + self.related_url = report.check_metadata.RelatedUrl + self.remediation_recommendation_text = report.check_metadata.Remediation[ + "Recommendation" + ]["Text"] + self.remediation_recommendation_url = report.check_metadata.Remediation[ + "Recommendation" + ]["Url"] + self.remediation_recommendation_code_nativeiac = ( + report.check_metadata.Remediation["Code"]["NativeIaC"] + ) + self.remediation_recommendation_code_terraform = ( + report.check_metadata.Remediation["Code"]["Terraform"] + ) + self.remediation_recommendation_code_cli = report.check_metadata.Remediation[ + "Code" + ]["cli"] + self.remediation_recommendation_code_cli = report.check_metadata.Remediation[ + "Code" + ]["cli"] + self.remediation_recommendation_code_other = report.check_metadata.Remediation[ + "Code" + ]["other"] + self.categories = self.__unroll_list__(report.check_metadata.Categories) + self.depends_on = self.__unroll_list__(report.check_metadata.DependsOn) + self.related_to = self.__unroll_list__(report.check_metadata.RelatedTo) + self.notes = report.check_metadata.Notes + self.compliance = self.__unroll_compliance__(report.check_metadata.Compliance) + + def __unroll_list__(self, listed_items: list): + unrolled_items = "" + separator = "|" + for item in listed_items: + if not unrolled_items: + unrolled_items = f"{item}" + else: + unrolled_items = f"{unrolled_items}{separator}{item}" + + return unrolled_items + + def __unroll_dict__(self, dict_items: dict): + unrolled_items = "" + separator = "|" + for key, value in dict_items.items(): + unrolled_item = f"{key}:{value}" + if not unrolled_items: + unrolled_items = f"{unrolled_item}" + else: + unrolled_items = f"{unrolled_items}{separator}{unrolled_item}" + + return unrolled_items + + def __unroll_compliance__(self, compliance: list): + compliance_frameworks = [] + # fill list of dataclasses + for item in compliance: + compliance_framework = Compliance_Framework( + Framework=item["Framework"], + Version=item["Version"], + Group=item["Group"], + Control=item["Control"], + ) + compliance_frameworks.append(compliance_framework) + # iterate over list of dataclasses to output info + unrolled_compliance = "" + groups = "" + controls = "" + item_separator = "," + framework_separator = "|" + generic_separator = "/" + for framework in compliance_frameworks: + for group in framework.Group: + if groups: + groups = f"{groups}{generic_separator}" + groups = f"{groups}{group}" + for control in framework.Control: + if controls: + controls = f"{controls}{generic_separator}" + controls = f"{controls}{control}" + + if unrolled_compliance: + unrolled_compliance = f"{unrolled_compliance}{framework_separator}" + unrolled_compliance = f"{unrolled_compliance}{framework.Framework}{item_separator}{framework.Version}{item_separator}{groups}{item_separator}{controls}" + # unset groups and controls for next framework + controls = "" + groups = "" + + return unrolled_compliance + + def get_csv_header(self): + csv_header = [] + for key in asdict(self): + csv_header = csv_header.append(key) + return csv_header diff --git a/lib/outputs/outputs.py b/lib/outputs/outputs.py new file mode 100644 index 00000000..ac790de9 --- /dev/null +++ b/lib/outputs/outputs.py @@ -0,0 +1,119 @@ +from csv import DictWriter + +from colorama import Fore, Style + +from config.config import csv_file_suffix +from lib.check.models import Organizations_Info +from lib.outputs.models import Check_Output_CSV +from lib.utils.utils import file_exists, open_file + + +def report(check_findings, output_options, audit_info, organizations_info): + check_findings.sort(key=lambda x: x.region) + + csv_fields = [] + # check output options + file_descriptors = {} + if output_options.output_modes: + if "csv" in output_options.output_modes: + csv_fields = generate_csv_fields() + + file_descriptors = fill_file_descriptors( + output_options.output_modes, + audit_info.audited_account, + output_options.output_directory, + csv_fields, + ) + + for finding in check_findings: + # printing the finding ... + + color = set_report_color(finding.status) + if output_options.is_quiet and "FAIL" in finding.status: + print( + f"{color}{finding.status}{Style.RESET_ALL} {finding.region}: {finding.status_extended}" + ) + elif not output_options.is_quiet: + print( + f"{color}{finding.status}{Style.RESET_ALL} {finding.region}: {finding.status_extended}" + ) + if file_descriptors: + + # sending the finding to input options + if "csv" in file_descriptors: + finding_output = Check_Output_CSV( + audit_info.audited_account, + audit_info.profile, + finding, + organizations_info, + ) + + csv_writer = DictWriter( + file_descriptors["csv"], fieldnames=csv_fields, delimiter=";" + ) + csv_writer.writerow(finding_output.__dict__) + + if file_descriptors: + # Close all file descriptors + for file_descriptor in file_descriptors: + file_descriptors.get(file_descriptor).close() + + +def fill_file_descriptors(output_modes, audited_account, output_directory, csv_fields): + file_descriptors = {} + for output_mode in output_modes: + if output_mode == "csv": + filename = ( + f"{output_directory}/prowler-output-{audited_account}-{csv_file_suffix}" + ) + if file_exists(filename): + file_descriptor = open_file( + filename, + "a", + ) + else: + file_descriptor = open_file( + filename, + "a", + ) + csv_header = [x.upper() for x in csv_fields] + csv_writer = DictWriter( + file_descriptor, fieldnames=csv_header, delimiter=";" + ) + csv_writer.writeheader() + + file_descriptors.update({output_mode: file_descriptor}) + return file_descriptors + + +def set_report_color(status): + color = "" + if status == "PASS": + color = Fore.GREEN + elif status == "FAIL": + color = Fore.RED + elif status == "ERROR": + color = Fore.BLACK + elif status == "WARNING": + color = Fore.YELLOW + else: + raise Exception("Invalid Report Status. Must be PASS, FAIL, ERROR or WARNING") + return color + + +def generate_csv_fields(): + csv_fields = [] + for field in Check_Output_CSV.__dict__["__annotations__"].keys(): + csv_fields.append(field) + return csv_fields + + +def get_orgs_info(): + organizations_info = Organizations_Info( + account_details_email="", + account_details_name="", + account_details_arn="", + account_details_org="", + account_details_tags="", + ) + return organizations_info diff --git a/lib/utils/utils.py b/lib/utils/utils.py index 1cb826c1..4519bbe1 100644 --- a/lib/utils/utils.py +++ b/lib/utils/utils.py @@ -1,15 +1,15 @@ import json import sys from io import TextIOWrapper +from os.path import exists from typing import Any from lib.logger import logger -def open_file(input_file: str) -> TextIOWrapper: +def open_file(input_file: str, mode: str = "r") -> TextIOWrapper: try: - # First recover the available groups in groups.json - f = open(input_file) + f = open(input_file, mode) except Exception as e: logger.critical(f"{input_file}: {e.__class__.__name__}") sys.exit() @@ -26,3 +26,17 @@ def parse_json_file(input_file: TextIOWrapper) -> Any: sys.exit() else: return json_file + + +# check if file exists +def file_exists(filename: str): + try: + exists_filename = exists(filename) + except Exception as e: + logger.critical(f"{exists_filename.name}: {e.__class__.__name__}") + quit() + else: + if exists_filename: + return True + else: + return False diff --git a/providers/aws/aws_provider.py b/providers/aws/aws_provider.py index 5d46fe6f..7bae53cb 100644 --- a/providers/aws/aws_provider.py +++ b/providers/aws/aws_provider.py @@ -153,11 +153,15 @@ def provider_set_session( logger.info("Audit session is the original one") current_audit_info.audit_session = current_audit_info.original_session + # Setting default region of session if current_audit_info.audit_session.region_name: current_audit_info.profile_region = current_audit_info.audit_session.region_name else: current_audit_info.profile_region = "us-east-1" + + return current_audit_info + def validate_credentials(validate_session): diff --git a/providers/aws/services/ec2/ec2_ebs_snapshots_encrypted/ec2_ebs_snapshots_encrypted.metadata.json b/providers/aws/services/ec2/ec2_ebs_snapshots_encrypted/ec2_ebs_snapshots_encrypted.metadata.json index c03f26b1..ef7b5a94 100644 --- a/providers/aws/services/ec2/ec2_ebs_snapshots_encrypted/ec2_ebs_snapshots_encrypted.metadata.json +++ b/providers/aws/services/ec2/ec2_ebs_snapshots_encrypted/ec2_ebs_snapshots_encrypted.metadata.json @@ -19,6 +19,17 @@ "level2" ], "Version": "1.4" + }, + { + "Control": [ + "4.4" + ], + "Framework": "PCI-DSS", + "Group": [ + "level1", + "level2" + ], + "Version": "1.4" } ], "DependsOn": [ diff --git a/providers/aws/services/ec2/ec2_ebs_snapshots_encrypted/ec2_ebs_snapshots_encrypted.py b/providers/aws/services/ec2/ec2_ebs_snapshots_encrypted/ec2_ebs_snapshots_encrypted.py index 345db830..f0e36539 100644 --- a/providers/aws/services/ec2/ec2_ebs_snapshots_encrypted/ec2_ebs_snapshots_encrypted.py +++ b/providers/aws/services/ec2/ec2_ebs_snapshots_encrypted/ec2_ebs_snapshots_encrypted.py @@ -10,24 +10,24 @@ class ec2_ebs_snapshots_encrypted(Check): if hasattr(regional_client, "snapshots"): if regional_client.snapshots: for snapshot in regional_client.snapshots: + report = Check_Report(self.metadata) + report.region = region if snapshot["Encrypted"]: - report = Check_Report() report.status = "PASS" - report.result_extended = ( + report.status_extended = ( f"EBS Snapshot {snapshot['SnapshotId']} is encrypted" ) - report.region = region + report.resource_id = snapshot["SnapshotId"] else: - report = Check_Report() report.status = "FAIL" - report.result_extended = ( + report.status_extended = ( f"EBS Snapshot {snapshot['SnapshotId']} is unencrypted" ) - report.region = region + report.resource_id = snapshot["SnapshotId"] else: - report = Check_Report() + report = Check_Report(self.metadata) report.status = "PASS" - report.result_extended = "There are no EC2 EBS snapshots" + report.status_extended = "There are no EC2 EBS snapshots" report.region = region findings.append(report) diff --git a/providers/aws/services/iam/iam_disable_30_days_credentials/iam_disable_30_days_credentials.py b/providers/aws/services/iam/iam_disable_30_days_credentials/iam_disable_30_days_credentials.py index 8a5e5cf8..661c1e2f 100644 --- a/providers/aws/services/iam/iam_disable_30_days_credentials/iam_disable_30_days_credentials.py +++ b/providers/aws/services/iam/iam_disable_30_days_credentials/iam_disable_30_days_credentials.py @@ -13,7 +13,10 @@ class iam_disable_30_days_credentials(Check): if response: for user in response: - report = Check_Report() + report = Check_Report(self.metadata) + report.resource_id = user["UserName"] + report.resource_arn = user["Arn"] + report.region = "us-east-1" if "PasswordLastUsed" in user and user["PasswordLastUsed"] != "": try: time_since_insertion = ( @@ -22,23 +25,21 @@ class iam_disable_30_days_credentials(Check): ) if time_since_insertion.days > maximum_expiration_days: report.status = "FAIL" - report.result_extended = f"User {user['UserName']} has not logged into the console in the past 30 days" - report.region = iam_client.region + report.status_extended = f"User {user['UserName']} has not logged into the console in the past 30 days" else: report.status = "PASS" - report.result_extended = f"User {user['UserName']} has logged into the console in the past 30 days" - report.region = iam_client.region + report.status_extended = f"User {user['UserName']} has logged into the console in the past 30 days" + except KeyError: pass else: report.status = "PASS" - report.result_extended = f"User {user['UserName']} has not a console password or is unused." - report.region = iam_client.region + report.status_extended = f"User {user['UserName']} has not a console password or is unused." # Append report findings.append(report) else: - report = Check_Report() + report = Check_Report(self.metadata) report.status = "PASS" report.result_extended = "There is no IAM users" report.region = iam_client.region diff --git a/providers/aws/services/iam/iam_disable_90_days_credentials/iam_disable_90_days_credentials.py b/providers/aws/services/iam/iam_disable_90_days_credentials/iam_disable_90_days_credentials.py index 8d565d13..0ed41797 100644 --- a/providers/aws/services/iam/iam_disable_90_days_credentials/iam_disable_90_days_credentials.py +++ b/providers/aws/services/iam/iam_disable_90_days_credentials/iam_disable_90_days_credentials.py @@ -13,7 +13,10 @@ class iam_disable_90_days_credentials(Check): if response: for user in response: - report = Check_Report() + report = Check_Report(self.metadata) + report.region = "us-east-1" + report.resource_id = user["UserName"] + report.resource_arn = user["Arn"] if "PasswordLastUsed" in user and user["PasswordLastUsed"] != "": try: time_since_insertion = ( @@ -22,25 +25,23 @@ class iam_disable_90_days_credentials(Check): ) if time_since_insertion.days > maximum_expiration_days: report.status = "FAIL" - report.result_extended = f"User {user['UserName']} has not logged into the console in the past 90 days" - report.region = iam_client.region + report.status_extended = f"User {user['UserName']} has not logged into the console in the past 90 days" else: report.status = "PASS" - report.result_extended = f"User {user['UserName']} has logged into the console in the past 90 days" - report.region = iam_client.region + report.status_extended = f"User {user['UserName']} has logged into the console in the past 90 days" + except KeyError: pass else: report.status = "PASS" - report.result_extended = f"User {user['UserName']} has not a console password or is unused." - report.region = iam_client.region + report.status_extended = f"User {user['UserName']} has not a console password or is unused." # Append report findings.append(report) else: - report = Check_Report() + report = Check_Report(self.metadata) report.status = "PASS" - report.result_extended = "There is no IAM users" - report.region = iam_client.region + report.status_extended = "There is no IAM users" + report.region = "us-east-1" return findings diff --git a/prowler b/prowler index fe6bfa01..84645798 100755 --- a/prowler +++ b/prowler @@ -3,7 +3,10 @@ import argparse import sys +from os import mkdir +from os.path import isdir +from config.config import default_output_directory from lib.banner import print_banner, print_version from lib.check.check import ( bulk_load_checks_metadata, @@ -38,7 +41,7 @@ if __name__ == "__main__": "--severity", nargs="+", help="List of severities [informational, low, medium, high, critical]", - choices=["informational","low","medium","high","critical"] + choices=["informational", "low", "medium", "high", "critical"], ) # Exclude checks options parser.add_argument("-e", "--excluded-checks", nargs="+", help="Checks to exclude") @@ -116,6 +119,21 @@ if __name__ == "__main__": nargs="+", help="AWS region names to run Prowler against", ) + parser.add_argument( + "-M", + "--output-modes", + nargs="+", + help="Output mode, by default csv", + choices=["csv"], + ) + parser.add_argument( + "-o", + "--custom-output-directory", + nargs="?", + help="Custom output directory, by default the folder where Prowler is stored", + default=default_output_directory, + ) + # Parse Arguments args = parser.parse_args() @@ -127,7 +145,9 @@ if __name__ == "__main__": services = args.services groups = args.groups checks_file = args.checks_file + output_directory = args.custom_output_directory severities = args.severity + output_modes = args.output_modes # Set Logger configuration set_logging_config(args.log_file, args.log_level) @@ -141,6 +161,17 @@ if __name__ == "__main__": logger.critical("To use -I/-T options -R option is needed") sys.exit() + # Check output directory, if it is default and not created -> create it + # If is custom and not created -> error + if output_directory: + if not isdir(output_directory): + if output_directory == default_output_directory: + if output_modes: + mkdir(default_output_directory) + else: + logger.critical("Output directory does not exist") + sys.exit() + if args.version: print_version() sys.exit() @@ -192,10 +223,12 @@ if __name__ == "__main__": sys.exit() # Setting output options - set_output_options(args.quiet) + audit_output_options = set_output_options( + args.quiet, output_modes, output_directory + ) # Set global session - provider_set_session( + audit_info = provider_set_session( args.profile, args.role, args.session_duration, @@ -218,7 +251,7 @@ if __name__ == "__main__": check_to_execute = getattr(lib, check_name) c = check_to_execute() # Run check - run_check(c) + run_check(c, audit_info, audit_output_options) # If check does not exists in the provider or is from another provider except ModuleNotFoundError: