From cab32d2f94919748d3b9f1e5cc05d31a2613940e Mon Sep 17 00:00:00 2001 From: Sergio Garcia <38561120+sergargar@users.noreply.github.com> Date: Fri, 15 Mar 2024 12:22:10 +0100 Subject: [PATCH] feat(mutelist): add Mute List for all providers (#3548) --- docs/tutorials/mutelist.md | 38 ++++---- prowler/__main__.py | 3 +- prowler/config/azure_mutelist_example.yaml | 44 +++++++++ prowler/config/config.py | 10 +++ prowler/config/gcp_mutelist_example.yaml | 44 +++++++++ .../config/kubernetes_mutelist_example.yaml | 44 +++++++++ prowler/lib/check/check.py | 89 ++++++++++--------- prowler/lib/check/models.py | 2 + prowler/lib/cli/parser.py | 15 ++++ .../aws => }/lib/mutelist/__init__.py | 0 .../aws => }/lib/mutelist/mutelist.py | 63 +++++++++---- prowler/lib/outputs/common.py | 1 + prowler/lib/outputs/outputs.py | 21 +++-- prowler/lib/outputs/summary_table.py | 17 +++- prowler/providers/aws/aws_provider.py | 4 +- .../providers/aws/lib/arguments/arguments.py | 10 --- prowler/providers/azure/azure_provider.py | 25 +++--- prowler/providers/gcp/gcp_provider.py | 24 +++-- .../kubernetes/kubernetes_provider.py | 24 +++-- .../mutelist/fixtures/aws_mutelist.yaml} | 0 .../aws => }/lib/mutelist/mutelist_test.py | 40 +++++---- 21 files changed, 366 insertions(+), 152 deletions(-) create mode 100644 prowler/config/azure_mutelist_example.yaml create mode 100644 prowler/config/gcp_mutelist_example.yaml create mode 100644 prowler/config/kubernetes_mutelist_example.yaml rename prowler/{providers/aws => }/lib/mutelist/__init__.py (100%) rename prowler/{providers/aws => }/lib/mutelist/mutelist.py (84%) rename tests/{providers/aws/lib/mutelist/fixtures/mutelist.yaml => lib/mutelist/fixtures/aws_mutelist.yaml} (100%) rename tests/{providers/aws => }/lib/mutelist/mutelist_test.py (96%) diff --git a/docs/tutorials/mutelist.md b/docs/tutorials/mutelist.md index bb8e586516..ec2af11dc0 100644 --- a/docs/tutorials/mutelist.md +++ b/docs/tutorials/mutelist.md @@ -3,10 +3,25 @@ Sometimes you may find resources that are intentionally configured in a certain Mute List option works along with other options and adds a `MUTED` instead of `MANUAL`, `PASS` or `FAIL` to any output format. -You can use `-w`/`--mutelist-file` with the path of your mutelist yaml file, but first, let's review the syntax. +You can use `-w`/`--mutelist-file` with the path of your mutelist yaml file: +``` +prowler -w mutelist.yaml +``` ## Mute List Yaml File Syntax +???+ note + For Azure provider, the Account ID is the Subscription Name and the Region is the Location. + +???+ note + For GCP provider, the Account ID is the Project ID and the Region is the Zone. + +???+ note + For Kubernetes provider, the Account ID is the Cluster Name and the Region is the Namespace. + +The Mute List file is a YAML file with the following syntax: + +```yaml ### Account, Check and/or Region can be * to apply for all the cases. ### Resources and tags are lists that can have either Regex or Keywords. ### Tags is an optional list that matches on tuples of 'key=value' and are "ANDed" together. @@ -78,8 +93,8 @@ You can use `-w`/`--mutelist-file` with the path of your mutelist yaml file, but - "test" Tags: - "environment=prod" # Will ignore every resource except in account 123456789012 except the ones containing the string "test" and tag environment=prod - -## Mute specific regions +``` +## Mute specific AWS regions If you want to mute failed findings only in specific regions, create a file with the following syntax and run it with `prowler aws -w mutelist.yaml`: Mute List: @@ -94,20 +109,13 @@ If you want to mute failed findings only in specific regions, create a file with - "*" ## Default AWS Mute List -Prowler provides you a Default AWS Mute List with the AWS Resources that should be muted such as all resources created by AWS Control Tower when setting up a landing zone. -You can execute Prowler with this mutelist using the following command: -```sh -prowler aws --mutelist prowler/config/aws_mutelist.yaml -``` -## Supported Mute List Locations +For the AWS Provider, Prowler is executed with a Default AWS Mute List with the AWS Resources that should be muted such as all resources created by AWS Control Tower when setting up a landing zone. +You can see this Mute List file in [`prowler/config/aws_mutelist.yaml`](https://github.com/prowler-cloud/prowler/blob/master/prowler/config/aws_allowlist.yaml). -The mutelisting flag supports the following locations: +## Supported AWS Mute List Locations + +The mutelisting flag supports the following AWS locations when using the AWS Provider: -### Local file -You will need to pass the local path where your Mute List YAML file is located: -``` -prowler -w mutelist.yaml -``` ### AWS S3 URI You will need to pass the S3 URI where your Mute List YAML file was uploaded to your bucket: ``` diff --git a/prowler/__main__.py b/prowler/__main__.py index fc5f140f80..d8d929decc 100644 --- a/prowler/__main__.py +++ b/prowler/__main__.py @@ -173,8 +173,6 @@ def prowler(): checks_to_execute = sorted(checks_to_execute) # Setup Mute List - # TODO: this should be available for all the providers - # Move the argument to the Prowler level to be available for all if hasattr(args, "mutelist_file"): global_provider.mutelist = args.mutelist_file @@ -195,6 +193,7 @@ def prowler(): checks_to_execute, global_provider, custom_checks_metadata, + getattr(args, "mutelist_file", None), ) else: logger.error( diff --git a/prowler/config/azure_mutelist_example.yaml b/prowler/config/azure_mutelist_example.yaml new file mode 100644 index 0000000000..7fc2172d80 --- /dev/null +++ b/prowler/config/azure_mutelist_example.yaml @@ -0,0 +1,44 @@ +### Account, Check and/or Region can be * to apply for all the cases. +### Account == Azure Subscription and Region == Azure Location +### Resources and tags are lists that can have either Regex or Keywords. +### Tags is an optional list that matches on tuples of 'key=value' and are "ANDed" together. +### Use an alternation Regex to match one of multiple tags with "ORed" logic. +### For each check you can except Accounts, Regions, Resources and/or Tags. +########################### MUTE LIST EXAMPLE ########################### +Mute List: + Accounts: + "Azure subscription 1": + Checks: + "sqlserver_tde_encryption_enabled": + Regions: + - "westeurope" + Resources: + - "sqlserver1" # Will ignore sqlserver1 in check sqlserver_tde_encryption_enabled located in westeurope + - "sqlserver2" # Will ignore sqlserver2 in check sqlserver_tde_encryption_enabled located in westeurope + "defender_*": + Regions: + - "*" + Resources: + - "*" # Will ignore every Defender check in every location + "*": + Regions: + - "*" + Resources: + - "test" + Tags: + - "test=test" # Will ignore every resource containing the string "test" and the tags 'test=test' and + - "project=test|project=stage" # either of ('project=test' OR project=stage) in Azure subscription 1 and every location + + "*": + Checks: + "vm_*": + Regions: + - "*" + Resources: + - "*" + Exceptions: + Accounts: + - "Subscription2" + Regions: + - "eastus" + - "eastus2" # Will ignore every resource in VM checks except the ones in Azure Subscription2 located in eastus or eastus2 diff --git a/prowler/config/config.py b/prowler/config/config.py index ac585253ce..24456048f7 100644 --- a/prowler/config/config.py +++ b/prowler/config/config.py @@ -63,6 +63,16 @@ default_config_file_path = ( ) +def get_default_mute_file_path(provider: str): + """ + get_default_mute_file_path returns the default mute file path for the provider + """ + # TODO: crate default mutelist file for kubernetes, azure and gcp + if provider == "aws": + return f"{pathlib.Path(os.path.dirname(os.path.realpath(__file__)))}/{provider}_mutelist.yaml" + return None + + def check_current_version(): try: prowler_version_string = f"Prowler {prowler_version}" diff --git a/prowler/config/gcp_mutelist_example.yaml b/prowler/config/gcp_mutelist_example.yaml new file mode 100644 index 0000000000..1631c9c85d --- /dev/null +++ b/prowler/config/gcp_mutelist_example.yaml @@ -0,0 +1,44 @@ +### Account, Check and/or Region can be * to apply for all the cases. +### Account == GCP Project ID and Region == GCP Location +### Resources and tags are lists that can have either Regex or Keywords. +### Tags is an optional list that matches on tuples of 'key=value' and are "ANDed" together. +### Use an alternation Regex to match one of multiple tags with "ORed" logic. +### For each check you can except Accounts, Regions, Resources and/or Tags. +########################### MUTE LIST EXAMPLE ########################### +Mute List: + Accounts: + "gcp-project-id-1": + Checks: + "compute_instance_public_ip": + Regions: + - "europe-southwest1" + Resources: + - "instance1" # Will ignore instance1 in check compute_instance_public_ip located in europe-southwest1 + - "instance2" # Will ignore instance2 in check compute_instance_public_ip located in europe-southwest1 + "iam_*": + Regions: + - "*" + Resources: + - "*" # Will ignore every IAM check in every location + "*": + Regions: + - "*" + Resources: + - "test" + Tags: + - "test=test" # Will ignore every resource containing the string "test" and the tags 'test=test' and + - "project=test|project=stage" # either of ('project=test' OR project=stage) in GCP Project gcp-project-id-1 and every location + + "*": + Checks: + "kms_*": + Regions: + - "*" + Resources: + - "*" + Exceptions: + Accounts: + - "gcp-project-id-2" + Regions: + - "us-west1" + - "us-west2" # Will ignore every resource in KMS checks except the ones in GCP Project gcp-project-id-2 located in us-west1 or us-west2 diff --git a/prowler/config/kubernetes_mutelist_example.yaml b/prowler/config/kubernetes_mutelist_example.yaml new file mode 100644 index 0000000000..727d3bebf6 --- /dev/null +++ b/prowler/config/kubernetes_mutelist_example.yaml @@ -0,0 +1,44 @@ +### Account, Check and/or Region can be * to apply for all the cases. +### Account == and Region == +### Resources and tags are lists that can have either Regex or Keywords. +### Tags is an optional list that matches on tuples of 'key=value' and are "ANDed" together. +### Use an alternation Regex to match one of multiple tags with "ORed" logic. +### For each check you can except Accounts, Regions, Resources and/or Tags. +########################### MUTE LIST EXAMPLE ########################### +Mute List: + Accounts: + "k8s-cluster-1": + Checks: + "core_minimize_allowPrivilegeEscalation_containers": + Regions: + - "namespace1" + Resources: + - "prowler-pod1" # Will ignore prowler-pod1 in check core_minimize_allowPrivilegeEscalation_containers located in namespace1 + - "prowler-pod2" # Will ignore prowler-pod2 in check core_minimize_allowPrivilegeEscalation_containers located in namespace1 + "kubelet_*": + Regions: + - "*" + Resources: + - "*" # Will ignore every Kubelet check in every namespace + "*": + Regions: + - "*" + Resources: + - "test" + Tags: + - "test=test" # Will ignore every resource containing the string "test" and the tags 'test=test' and + - "project=test|project=stage" # either of ('project=test' OR project=stage) in Kubernetes Cluster k8s-cluster-1 and every namespace + + "*": + Checks: + "etcd_*": + Regions: + - "*" + Resources: + - "*" + Exceptions: + Accounts: + - "k8s-cluster-2" + Regions: + - "namespace1" + - "namespace2" # Will ignore every ETCD finding except the ones in Kubernetes Cluster k8s-cluster-2 located in namespace1 or namespace2 diff --git a/prowler/lib/check/check.py b/prowler/lib/check/check.py index 01ec4a74c8..2b22838a71 100644 --- a/prowler/lib/check/check.py +++ b/prowler/lib/check/check.py @@ -19,9 +19,9 @@ from prowler.lib.check.compliance_models import load_compliance_framework from prowler.lib.check.custom_checks_metadata import update_check_metadata from prowler.lib.check.models import Check, load_check_metadata from prowler.lib.logger import logger +from prowler.lib.mutelist.mutelist import mutelist_findings from prowler.lib.outputs.outputs import report from prowler.lib.utils.utils import open_file, parse_json_file -from prowler.providers.aws.lib.mutelist.mutelist import mutelist_findings from prowler.providers.common.common import get_global_provider from prowler.providers.common.models import Audit_Metadata @@ -422,6 +422,7 @@ def execute_checks( checks_to_execute: list, global_provider: Any, custom_checks_metadata: Any, + mutelist_file: str, ) -> list: # List to store all the check's findings all_findings = [] @@ -484,6 +485,11 @@ def execute_checks( f"{check_name} - {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) else: + # Print the mutelist (if any) that is being used + if mutelist_file: + print( + f"{Style.BRIGHT}Using the following Mute List: {Style.RESET_ALL}{Fore.YELLOW}{mutelist_file}{Style.RESET_ALL}\n" + ) # Default execution checks_num = len(checks_to_execute) plural_string = "checks" @@ -542,52 +548,55 @@ def execute( checks_executed: set, custom_checks_metadata: Any, ): - # Import check module - check_module_path = f"prowler.providers.{global_provider.type}.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() + try: + # Import check module + check_module_path = f"prowler.providers.{global_provider.type}.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() - # 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]) + # 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]) - # Run check - check_findings = run_check(c, global_provider.output_options) + # Run check + check_findings = run_check(c, global_provider.output_options) - # Update Audit Status - services_executed.add(service) - checks_executed.add(check_name) - global_provider.audit_metadata = update_audit_metadata( - global_provider.audit_metadata, services_executed, checks_executed - ) - - # Mute List findings - if hasattr(global_provider, "mutelist") and global_provider.mutelist: - check_findings = mutelist_findings( - global_provider.mutelist, - global_provider.identity.account, - check_findings, + # Update Audit Status + services_executed.add(service) + checks_executed.add(check_name) + global_provider.audit_metadata = update_audit_metadata( + global_provider.audit_metadata, services_executed, checks_executed ) - # Report the check's findings - report(check_findings, global_provider) - - if os.environ.get("PROWLER_REPORT_LIB_PATH"): - try: - logger.info("Using custom report interface ...") - lib = os.environ["PROWLER_REPORT_LIB_PATH"] - outputs_module = importlib.import_module(lib) - custom_report_interface = getattr(outputs_module, "report") - - # TODO: review this call and see if we can remove the global_provider.output_options since it is contained in the global_provider - custom_report_interface( - check_findings, global_provider.output_options, global_provider + # Mute List findings + if hasattr(global_provider, "mutelist") and global_provider.mutelist: + check_findings = mutelist_findings( + global_provider, + check_findings, ) - except Exception: - sys.exit(1) + # Report the check's findings + report(check_findings, global_provider) + + if os.environ.get("PROWLER_REPORT_LIB_PATH"): + try: + logger.info("Using custom report interface ...") + lib = os.environ["PROWLER_REPORT_LIB_PATH"] + outputs_module = importlib.import_module(lib) + custom_report_interface = getattr(outputs_module, "report") + + # TODO: review this call and see if we can remove the global_provider.output_options since it is contained in the global_provider + custom_report_interface( + check_findings, global_provider.output_options, global_provider + ) + except Exception: + sys.exit(1) + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) return check_findings diff --git a/prowler/lib/check/models.py b/prowler/lib/check/models.py index 2c426e1f31..f23873dd3e 100644 --- a/prowler/lib/check/models.py +++ b/prowler/lib/check/models.py @@ -115,6 +115,7 @@ class Check_Report: check_metadata: Check_Metadata_Model resource_details: str resource_tags: list + muted: bool def __init__(self, metadata): self.status = "" @@ -122,6 +123,7 @@ class Check_Report: self.status_extended = "" self.resource_details = "" self.resource_tags = [] + self.muted = False @dataclass diff --git a/prowler/lib/cli/parser.py b/prowler/lib/cli/parser.py index 691e710bf4..a0118e597b 100644 --- a/prowler/lib/cli/parser.py +++ b/prowler/lib/cli/parser.py @@ -8,6 +8,7 @@ from prowler.config.config import ( default_config_file_path, default_output_directory, finding_statuses, + get_default_mute_file_path, valid_severities, ) from prowler.providers.common.arguments import ( @@ -50,6 +51,7 @@ Detailed documentation at https://docs.prowler.cloud self.__init_checks_parser__() self.__init_exclude_checks_parser__() self.__init_list_checks_parser__() + self.__init_mutelist_parser__() self.__init_config_parser__() self.__init_custom_checks_metadata_parser__() self.__init_third_party_integrations_parser__() @@ -282,6 +284,19 @@ Detailed documentation at https://docs.prowler.cloud help="List the available check's categories", ) + def __init_mutelist_parser__(self): + mutelist_subparser = self.common_providers_parser.add_argument_group( + "Mute List" + ) + provider = sys.argv[1] if len(sys.argv) > 1 else "aws" + mutelist_subparser.add_argument( + "-w", + "--mutelist-file", + nargs="?", + default=get_default_mute_file_path(provider), + help="Path for mutelist yaml file. See example prowler/config/_mutelist.yaml for reference and format. For AWS provider, it also accepts AWS DynamoDB Table, Lambda ARNs or S3 URIs, see more in https://docs.prowler.cloud/en/latest/tutorials/mutelist/", + ) + def __init_config_parser__(self): config_parser = self.common_providers_parser.add_argument_group("Configuration") config_parser.add_argument( diff --git a/prowler/providers/aws/lib/mutelist/__init__.py b/prowler/lib/mutelist/__init__.py similarity index 100% rename from prowler/providers/aws/lib/mutelist/__init__.py rename to prowler/lib/mutelist/__init__.py diff --git a/prowler/providers/aws/lib/mutelist/mutelist.py b/prowler/lib/mutelist/mutelist.py similarity index 84% rename from prowler/providers/aws/lib/mutelist/mutelist.py rename to prowler/lib/mutelist/mutelist.py index a69615a4e8..8cb99e8e64 100644 --- a/prowler/providers/aws/lib/mutelist/mutelist.py +++ b/prowler/lib/mutelist/mutelist.py @@ -33,20 +33,22 @@ mutelist_schema = Schema( ) -def parse_mutelist_file(session: Session, aws_account: str, mutelist_path: str): +def parse_mutelist_file( + mutelist_path: str, aws_session: Session = None, aws_account: str = None +): try: # Check if file is a S3 URI if re.search("^s3://([^/]+)/(.*?([^/]+))$", mutelist_path): bucket = mutelist_path.split("/")[2] key = ("/").join(mutelist_path.split("/")[3:]) - s3_client = session.client("s3") + s3_client = aws_session.client("s3") mutelist = yaml.safe_load( s3_client.get_object(Bucket=bucket, Key=key)["Body"] )["Mute List"] # Check if file is a Lambda Function ARN elif re.search(r"^arn:(\w+):lambda:", mutelist_path): lambda_region = mutelist_path.split(":")[3] - lambda_client = session.client("lambda", region_name=lambda_region) + lambda_client = aws_session.client("lambda", region_name=lambda_region) lambda_response = lambda_client.invoke( FunctionName=mutelist_path, InvocationType="RequestResponse" ) @@ -59,7 +61,9 @@ def parse_mutelist_file(session: Session, aws_account: str, mutelist_path: str): ): mutelist = {"Accounts": {}} table_region = mutelist_path.split(":")[3] - dynamodb_resource = session.resource("dynamodb", region_name=table_region) + dynamodb_resource = aws_session.resource( + "dynamodb", region_name=table_region + ) dynamo_table = dynamodb_resource.Table(mutelist_path.split("/")[1]) response = dynamo_table.scan( FilterExpression=Attr("Accounts").is_in([aws_account, "*"]) @@ -109,21 +113,50 @@ def parse_mutelist_file(session: Session, aws_account: str, mutelist_path: str): def mutelist_findings( - mutelist: dict, - audited_account: str, + global_provider: Any, check_findings: list[Any], ): # Check if finding is muted for finding in check_findings: - if is_muted( - mutelist, - audited_account, - finding.check_metadata.CheckID, - finding.region, - finding.resource_id, - unroll_tags(finding.resource_tags), - ): - finding.status = "MUTED" + # TODO: Move this mapping to the execute_check function and pass that output to the mutelist and the report + if global_provider.type == "aws": + finding.muted = is_muted( + global_provider.mutelist, + global_provider.identity.account, + finding.check_metadata.CheckID, + finding.region, + finding.resource_id, + unroll_tags(finding.resource_tags), + ) + elif global_provider.type == "azure": + finding.muted = is_muted( + global_provider.mutelist, + finding.subscription, + finding.check_metadata.CheckID, + # TODO: add region to the findings when we add Azure Locations + # finding.region, + "", + finding.resource_name, + unroll_tags(finding.resource_tags), + ) + elif global_provider.type == "gcp": + finding.muted = is_muted( + global_provider.mutelist, + finding.project_id, + finding.check_metadata.CheckID, + finding.location, + finding.resource_name, + unroll_tags(finding.resource_tags), + ) + elif global_provider.type == "kubernetes": + finding.muted = is_muted( + global_provider.mutelist, + global_provider.identity.cluster, + finding.check_metadata.CheckID, + finding.namespace, + finding.resource_name, + unroll_tags(finding.resource_tags), + ) return check_findings diff --git a/prowler/lib/outputs/common.py b/prowler/lib/outputs/common.py index d6c3b1512b..f6fc917860 100644 --- a/prowler/lib/outputs/common.py +++ b/prowler/lib/outputs/common.py @@ -111,6 +111,7 @@ def fill_common_finding_data(finding: dict, unix_timestamp: bool) -> dict: "check_type": ",".join(finding.check_metadata.CheckType), "status": finding.status, "status_extended": finding.status_extended, + "muted": finding.muted, "service_name": finding.check_metadata.ServiceName, "subservice_name": finding.check_metadata.SubServiceName, "severity": finding.check_metadata.Severity, diff --git a/prowler/lib/outputs/outputs.py b/prowler/lib/outputs/outputs.py index a1f7989698..7dcd321f6b 100644 --- a/prowler/lib/outputs/outputs.py +++ b/prowler/lib/outputs/outputs.py @@ -35,9 +35,14 @@ def stdout_report(finding, color, verbose, status): details = finding.namespace.lower() if verbose and (not status or finding.status in status): - print( - f"\t{color}{finding.status}{Style.RESET_ALL} {details}: {finding.status_extended}" - ) + if finding.muted: + print( + f"\t{color}MUTED ({finding.status}){Style.RESET_ALL} {details}: {finding.status_extended}" + ) + else: + print( + f"\t{color}{finding.status}{Style.RESET_ALL} {details}: {finding.status_extended}" + ) def report(check_findings, provider): @@ -64,7 +69,7 @@ def report(check_findings, provider): for finding in check_findings: # Print findings by stdout - color = set_report_color(finding.status) + color = set_report_color(finding.status, finding.muted) stdout_report( finding, color, output_options.verbose, output_options.status ) @@ -163,17 +168,17 @@ def report(check_findings, provider): ) -def set_report_color(status: str) -> str: +def set_report_color(status: str, muted: bool = False) -> str: """Return the color for a give result status""" color = "" - if status == "PASS": + if muted: + color = orange_color + elif status == "PASS": color = Fore.GREEN elif status == "FAIL": color = Fore.RED elif status == "ERROR": color = Fore.BLACK - elif status == "MUTED": - color = orange_color elif status == "MANUAL": color = Fore.YELLOW else: diff --git a/prowler/lib/outputs/summary_table.py b/prowler/lib/outputs/summary_table.py index cb1a3d8412..b73741c963 100644 --- a/prowler/lib/outputs/summary_table.py +++ b/prowler/lib/outputs/summary_table.py @@ -7,6 +7,7 @@ from prowler.config.config import ( csv_file_suffix, json_asff_file_suffix, json_ocsf_file_suffix, + orange_color, ) from prowler.lib.logger import logger @@ -48,6 +49,7 @@ def display_summary_table( "High": 0, "Medium": 0, "Low": 0, + "Muted": 0, } findings_table = { "Provider": [], @@ -57,8 +59,9 @@ def display_summary_table( "High": [], "Medium": [], "Low": [], + "Muted": [], } - pass_count = fail_count = 0 + pass_count = fail_count = muted_count = 0 for finding in findings: # If new service and not first, add previous row if ( @@ -67,14 +70,17 @@ def display_summary_table( ): add_service_to_table(findings_table, current) - current["Total"] = current["Critical"] = current["High"] = current[ - "Medium" - ] = current["Low"] = 0 + current["Total"] = current["Muted"] = current["Critical"] = current[ + "High" + ] = current["Medium"] = current["Low"] = 0 current["Service"] = finding.check_metadata.ServiceName current["Provider"] = finding.check_metadata.Provider current["Total"] += 1 + if finding.muted: + muted_count += 1 + current["Muted"] += 1 if finding.status == "PASS": pass_count += 1 elif finding.status == "FAIL": @@ -97,6 +103,7 @@ def display_summary_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}", + f"{orange_color}{round(muted_count / len(findings) * 100, 2)}% ({muted_count}) Muted{Style.RESET_ALL}", ] ] print(tabulate(overview_table, tablefmt="rounded_grid")) @@ -149,6 +156,7 @@ def add_service_to_table(findings_table, current): current["Status"] = f"{Fore.RED}FAIL ({total_fails}){Style.RESET_ALL}" else: current["Status"] = f"{Fore.GREEN}PASS ({current['Total']}){Style.RESET_ALL}" + findings_table["Provider"].append(current["Provider"]) findings_table["Service"].append(current["Service"]) findings_table["Status"].append(current["Status"]) @@ -160,3 +168,4 @@ def add_service_to_table(findings_table, current): f"{Fore.YELLOW}{current['Medium']}{Style.RESET_ALL}" ) findings_table["Low"].append(f"{Fore.BLUE}{current['Low']}{Style.RESET_ALL}") + findings_table["Muted"].append(f"{orange_color}{current['Muted']}{Style.RESET_ALL}") diff --git a/prowler/providers/aws/aws_provider.py b/prowler/providers/aws/aws_provider.py index e5e81b5f9c..510a30f248 100644 --- a/prowler/providers/aws/aws_provider.py +++ b/prowler/providers/aws/aws_provider.py @@ -13,6 +13,7 @@ from colorama import Fore, Style from prowler.config.config import aws_services_json_file, load_and_validate_config_file from prowler.lib.check.check import list_modules, recover_checks_from_service from prowler.lib.logger import logger +from prowler.lib.mutelist.mutelist import parse_mutelist_file from prowler.lib.utils.utils import open_file, parse_json_file from prowler.providers.aws.config import ( AWS_STS_GLOBAL_ENDPOINT_REGION, @@ -20,7 +21,6 @@ from prowler.providers.aws.config import ( ROLE_SESSION_NAME, ) from prowler.providers.aws.lib.arn.arn import parse_iam_credentials_arn -from prowler.providers.aws.lib.mutelist.mutelist import parse_mutelist_file from prowler.providers.aws.lib.organizations.organizations import ( get_organizations_metadata, parse_organizations_metadata, @@ -278,7 +278,7 @@ class AwsProvider(Provider): def mutelist(self, mutelist_path): if mutelist_path: mutelist = parse_mutelist_file( - self._session.current_session, self._identity.account, mutelist_path + mutelist_path, self._session.current_session, self._identity.account ) else: mutelist = {} diff --git a/prowler/providers/aws/lib/arguments/arguments.py b/prowler/providers/aws/lib/arguments/arguments.py index 97eb00fba9..fefe8f0a0e 100644 --- a/prowler/providers/aws/lib/arguments/arguments.py +++ b/prowler/providers/aws/lib/arguments/arguments.py @@ -120,16 +120,6 @@ def init_parser(self): help="Same as -B but do not use the assumed role credentials to put objects to the bucket, instead uses the initial credentials.", ) - # Mute List - mutelist_subparser = aws_parser.add_argument_group("Mute List") - mutelist_subparser.add_argument( - "-w", - "--mutelist-file", - nargs="?", - default=None, - help="Path for mutelist yaml file. See example prowler/config/aws_mutelist.yaml for reference and format. It also accepts AWS DynamoDB Table or Lambda ARNs or S3 URIs, see more in https://docs.prowler.cloud/en/latest/tutorials/mutelist/", - ) - # Based Scans aws_based_scans_subparser = aws_parser.add_argument_group("AWS Based Scans") aws_based_scans_parser = aws_based_scans_subparser.add_mutually_exclusive_group() diff --git a/prowler/providers/azure/azure_provider.py b/prowler/providers/azure/azure_provider.py index 71fd62e1ba..8f4a3f62ff 100644 --- a/prowler/providers/azure/azure_provider.py +++ b/prowler/providers/azure/azure_provider.py @@ -10,6 +10,7 @@ from msgraph import GraphServiceClient from prowler.config.config import load_and_validate_config_file from prowler.lib.logger import logger +from prowler.lib.mutelist.mutelist import parse_mutelist_file from prowler.providers.azure.lib.regions.regions import get_regions_config from prowler.providers.azure.models import ( AzureIdentityInfo, @@ -124,20 +125,18 @@ class AzureProvider(Provider): "partition": "region_config.name", } - # TODO: pending to implement - # @property - # def mutelist(self): - # return self._mutelist + @property + def mutelist(self): + return self._mutelist - # @mutelist.setter - # def mutelist(self, mutelist_path): - # if mutelist_path: - # mutelist = parse_mutelist_file( - # self._session.current_session, self._identity.account, mutelist_path - # ) - # else: - # mutelist = {} - # self._mutelist = mutelist + @mutelist.setter + def mutelist(self, mutelist_path): + if mutelist_path: + mutelist = parse_mutelist_file(mutelist_path) + else: + mutelist = {} + + self._mutelist = mutelist # TODO: this should be moved to the argparse, if not we need to enforce it from the Provider def validate_arguments( diff --git a/prowler/providers/gcp/gcp_provider.py b/prowler/providers/gcp/gcp_provider.py index 9cb8428675..fefde52ccc 100644 --- a/prowler/providers/gcp/gcp_provider.py +++ b/prowler/providers/gcp/gcp_provider.py @@ -9,6 +9,7 @@ from googleapiclient.errors import HttpError from prowler.config.config import load_and_validate_config_file from prowler.lib.logger import logger +from prowler.lib.mutelist.mutelist import parse_mutelist_file from prowler.providers.common.models import Audit_Metadata from prowler.providers.common.provider import Provider from prowler.providers.gcp.models import ( @@ -132,20 +133,17 @@ class GcpProvider(Provider): # "partition": "identity.partition", } - # TODO: pending to implement - # @property - # def mutelist(self): - # return self._mutelist + @property + def mutelist(self): + return self._mutelist - # @mutelist.setter - # def mutelist(self, mutelist_path): - # if mutelist_path: - # mutelist = parse_mutelist_file( - # self._session.current_session, self._identity.account, mutelist_path - # ) - # else: - # mutelist = {} - # self._mutelist = mutelist + @mutelist.setter + def mutelist(self, mutelist_path): + if mutelist_path: + mutelist = parse_mutelist_file(mutelist_path) + else: + mutelist = {} + self._mutelist = mutelist def setup_session(self, credentials_file): try: diff --git a/prowler/providers/kubernetes/kubernetes_provider.py b/prowler/providers/kubernetes/kubernetes_provider.py index 6319209b0e..c00ccaa502 100644 --- a/prowler/providers/kubernetes/kubernetes_provider.py +++ b/prowler/providers/kubernetes/kubernetes_provider.py @@ -7,6 +7,7 @@ from kubernetes import client, config from prowler.config.config import load_and_validate_config_file from prowler.lib.logger import logger +from prowler.lib.mutelist.mutelist import parse_mutelist_file from prowler.providers.common.models import Audit_Metadata from prowler.providers.common.provider import Provider from prowler.providers.kubernetes.models import ( @@ -105,20 +106,17 @@ class KubernetesProvider(Provider): # "partition": "identity.partition", } - # TODO: pending to implement - # @property - # def mutelist(self): - # return self._mutelist + @property + def mutelist(self): + return self._mutelist - # @mutelist.setter - # def mutelist(self, mutelist_path): - # if mutelist_path: - # mutelist = parse_mutelist_file( - # self._session.current_session, self._identity.account, mutelist_path - # ) - # else: - # mutelist = {} - # self._mutelist = mutelist + @mutelist.setter + def mutelist(self, mutelist_path): + if mutelist_path: + mutelist = parse_mutelist_file(mutelist_path) + else: + mutelist = {} + self._mutelist = mutelist def setup_session(self, kubeconfig_file, input_context) -> KubernetesSession: """ diff --git a/tests/providers/aws/lib/mutelist/fixtures/mutelist.yaml b/tests/lib/mutelist/fixtures/aws_mutelist.yaml similarity index 100% rename from tests/providers/aws/lib/mutelist/fixtures/mutelist.yaml rename to tests/lib/mutelist/fixtures/aws_mutelist.yaml diff --git a/tests/providers/aws/lib/mutelist/mutelist_test.py b/tests/lib/mutelist/mutelist_test.py similarity index 96% rename from tests/providers/aws/lib/mutelist/mutelist_test.py rename to tests/lib/mutelist/mutelist_test.py index f25c5268fa..b68b579e01 100644 --- a/tests/providers/aws/lib/mutelist/mutelist_test.py +++ b/tests/lib/mutelist/mutelist_test.py @@ -3,7 +3,7 @@ from boto3 import resource from mock import MagicMock from moto import mock_aws -from prowler.providers.aws.lib.mutelist.mutelist import ( +from prowler.lib.mutelist.mutelist import ( is_excepted, is_muted, is_muted_in_check, @@ -19,7 +19,7 @@ from tests.providers.aws.utils import ( AWS_REGION_EU_SOUTH_3, AWS_REGION_EU_WEST_1, AWS_REGION_US_EAST_1, - set_mocked_aws_audit_info, + set_mocked_aws_provider, ) @@ -27,28 +27,28 @@ class TestMutelist: # Test S3 mutelist @mock_aws def test_s3_mutelist(self): - audit_info = set_mocked_aws_audit_info() + aws_provider = set_mocked_aws_provider() # Create bucket and upload mutelist yaml s3_resource = resource("s3", region_name=AWS_REGION_US_EAST_1) s3_resource.create_bucket(Bucket="test-mutelist") s3_resource.Object("test-mutelist", "mutelist.yaml").put( Body=open( - "tests/providers/aws/lib/mutelist/fixtures/mutelist.yaml", + "tests//lib/mutelist/fixtures/aws_mutelist.yaml", "rb", ) ) - with open("tests/providers/aws/lib/mutelist/fixtures/mutelist.yaml") as f: + with open("tests//lib/mutelist/fixtures/aws_mutelist.yaml") as f: assert yaml.safe_load(f)["Mute List"] == parse_mutelist_file( - audit_info.session.current_session, - audit_info.identity.account, "s3://test-mutelist/mutelist.yaml", + aws_provider.session.current_session, + aws_provider.identity.account, ) # Test DynamoDB mutelist @mock_aws def test_dynamo_mutelist(self): - audit_info = set_mocked_aws_audit_info() + aws_provider = set_mocked_aws_provider() # Create table and put item dynamodb_resource = resource("dynamodb", region_name=AWS_REGION_US_EAST_1) table_name = "test-mutelist" @@ -80,20 +80,20 @@ class TestMutelist: assert ( "keyword" in parse_mutelist_file( - audit_info.session.current_session, - audit_info.identity.account, "arn:aws:dynamodb:" + AWS_REGION_US_EAST_1 + ":" + str(AWS_ACCOUNT_NUMBER) + ":table/" + table_name, + aws_provider.session.current_session, + aws_provider.identity.account, )["Accounts"]["*"]["Checks"]["iam_user_hardware_mfa_enabled"]["Resources"] ) @mock_aws def test_dynamo_mutelist_with_tags(self): - audit_info = set_mocked_aws_audit_info() + aws_provider = set_mocked_aws_provider() # Create table and put item dynamodb_resource = resource("dynamodb", region_name=AWS_REGION_US_EAST_1) table_name = "test-mutelist" @@ -126,14 +126,14 @@ class TestMutelist: assert ( "environment=dev" in parse_mutelist_file( - audit_info.session.current_session, - audit_info.identity.account, "arn:aws:dynamodb:" + AWS_REGION_US_EAST_1 + ":" + str(AWS_ACCOUNT_NUMBER) + ":table/" + table_name, + aws_provider.session.current_session, + aws_provider.identity.account, )["Accounts"]["*"]["Checks"]["*"]["Tags"] ) @@ -161,12 +161,15 @@ class TestMutelist: finding_1.region = AWS_REGION_US_EAST_1 finding_1.resource_id = "prowler" finding_1.resource_tags = [] + aws_provider = set_mocked_aws_provider() + aws_provider._mutelist = mutelist check_findings.append(finding_1) - muted_findings = mutelist_findings(mutelist, AWS_ACCOUNT_NUMBER, check_findings) + muted_findings = mutelist_findings(aws_provider, check_findings) assert len(muted_findings) == 1 - assert muted_findings[0].status == "MUTED" + assert muted_findings[0].status == "FAIL" + assert muted_findings[0].muted def test_mutelist_all_exceptions_empty(self): @@ -199,12 +202,15 @@ class TestMutelist: finding_1.region = AWS_REGION_US_EAST_1 finding_1.resource_id = "prowler" finding_1.resource_tags = [] + aws_provider = set_mocked_aws_provider() + aws_provider._mutelist = mutelist check_findings.append(finding_1) - muted_findings = mutelist_findings(mutelist, AWS_ACCOUNT_NUMBER, check_findings) + muted_findings = mutelist_findings(aws_provider, check_findings) assert len(muted_findings) == 1 - assert muted_findings[0].status == "MUTED" + assert muted_findings[0].status == "FAIL" + assert muted_findings[0].muted def test_is_muted_with_everything_excepted(self): mutelist = {