diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 4462e7a587..dba86e23c2 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -41,6 +41,8 @@ All notable changes to the **Prowler SDK** are documented in this file. - Github provider to `usage` section of `prowler -h`: [(#7906)](https://github.com/prowler-cloud/prowler/pull/7906) - `network_flow_log_more_than_90_days` check to pass when retention policy is 0 days [(#7975)](https://github.com/prowler-cloud/prowler/pull/7975) - Update SDK Azure call for ftps_state in the App Service [(#7923)](https://github.com/prowler-cloud/prowler/pull/7923) +- Validate ResourceType in CheckMetadata [(#8035)](https://github.com/prowler-cloud/prowler/pull/8035) +- Missing ResourceType values in check's metadata [(#8028)](https://github.com/prowler-cloud/prowler/pull/8028) --- diff --git a/prowler/lib/check/models.py b/prowler/lib/check/models.py index 9c8ee40eb6..da670a9dda 100644 --- a/prowler/lib/check/models.py +++ b/prowler/lib/check/models.py @@ -96,6 +96,7 @@ class CheckMetadata(BaseModel): severity_to_lower(severity): Validator function to convert the severity to lowercase. valid_severity(severity): Validator function to validate the severity of the check. valid_cli_command(remediation): Validator function to validate the CLI command is not an URL. + valid_resource_type(resource_type): Validator function to validate the resource type is not empty. """ Provider: str @@ -141,6 +142,12 @@ class CheckMetadata(BaseModel): raise ValueError("CLI command cannot be an URL") return remediation + @validator("ResourceType", pre=True, always=True) + def valid_resource_type(resource_type): + if not resource_type or not isinstance(resource_type, str): + raise ValueError("ResourceType must be a non-empty string") + return resource_type + @staticmethod def get_bulk(provider: str) -> dict[str, "CheckMetadata"]: """ @@ -646,7 +653,6 @@ def load_check_metadata(metadata_file: str) -> CheckMetadata: check_metadata = CheckMetadata.parse_file(metadata_file) except ValidationError as error: logger.critical(f"Metadata from {metadata_file} is not valid: {error}") - # TODO: remove this exit and raise an exception - sys.exit(1) + raise error else: return check_metadata diff --git a/prowler/providers/aws/services/directconnect/directconnect_connection_redundancy/directconnect_connection_redundancy.metadata.json b/prowler/providers/aws/services/directconnect/directconnect_connection_redundancy/directconnect_connection_redundancy.metadata.json index fc13f982e4..42dbbe68dc 100644 --- a/prowler/providers/aws/services/directconnect/directconnect_connection_redundancy/directconnect_connection_redundancy.metadata.json +++ b/prowler/providers/aws/services/directconnect/directconnect_connection_redundancy/directconnect_connection_redundancy.metadata.json @@ -9,7 +9,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:directconnect:region:account-id:directconnect/resource-id", "Severity": "medium", - "ResourceType": "", + "ResourceType": "Other", "Description": "Checks the resilience of the AWS Direct Connect used to connect your on-premises.", "Risk": "This check alerts you if any Direct Connect connections are not redundant and the connections are coming from two distinct Direct Connect locations. Lack of location resiliency can result in unexpected downtime during maintenance, a fiber cut, a device failure, or a complete location failure.", "RelatedUrl": "https://docs.aws.amazon.com/awssupport/latest/user/fault-tolerance-checks.html#amazon-direct-connect-location-resiliency", diff --git a/prowler/providers/aws/services/directconnect/directconnect_virtual_interface_redundancy/directconnect_virtual_interface_redundancy.metadata.json b/prowler/providers/aws/services/directconnect/directconnect_virtual_interface_redundancy/directconnect_virtual_interface_redundancy.metadata.json index cf16a0664e..a42a71ed83 100644 --- a/prowler/providers/aws/services/directconnect/directconnect_virtual_interface_redundancy/directconnect_virtual_interface_redundancy.metadata.json +++ b/prowler/providers/aws/services/directconnect/directconnect_virtual_interface_redundancy/directconnect_virtual_interface_redundancy.metadata.json @@ -9,7 +9,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:directconnect:region:account-id:directconnect/resource-id", "Severity": "medium", - "ResourceType": "", + "ResourceType": "Other", "Description": "Checks the resilience of the AWS Direct Connect used to connect your on-premises to each Direct Connect gateway or virtual private gateway.", "Risk": "This check alerts you if any Direct Connect gateway or virtual private gateway isn't configured with virtual interfaces across at least two distinct Direct Connect locations. Lack of location resiliency can result in unexpected downtime during maintenance, a fiber cut, a device failure, or a complete location failure.", "RelatedUrl": "https://docs.aws.amazon.com/awssupport/latest/user/fault-tolerance-checks.html#amazon-direct-connect-location-resiliency", diff --git a/prowler/providers/azure/services/aisearch/aisearch_service_not_publicly_accessible/aisearch_service_not_publicly_accessible.metadata.json b/prowler/providers/azure/services/aisearch/aisearch_service_not_publicly_accessible/aisearch_service_not_publicly_accessible.metadata.json index 7130068a23..0eaadbec0f 100644 --- a/prowler/providers/azure/services/aisearch/aisearch_service_not_publicly_accessible/aisearch_service_not_publicly_accessible.metadata.json +++ b/prowler/providers/azure/services/aisearch/aisearch_service_not_publicly_accessible/aisearch_service_not_publicly_accessible.metadata.json @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "", + "ResourceType": "AzureSearchService", "Description": "Ensure that public network access to the Search Service is restricted.", "Risk": "Public accessibility exposes the Search Service to potential attacks, unauthorized usage, and data breaches. Restricting access minimizes the surface area for attacks and ensures that only authorized networks can access the search service.", "RelatedUrl": "https://learn.microsoft.com/en-us/azure/search/service-configure-firewall#configure-network-access-in-azure-portal", @@ -23,7 +23,9 @@ "Url": "https://learn.microsoft.com/en-us/azure/search/service-configure-firewall#configure-network-access-in-azure-portal" } }, - "Categories": ["gen-ai"], + "Categories": [ + "gen-ai" + ], "DependsOn": [], "RelatedTo": [], "Notes": "" diff --git a/prowler/providers/azure/services/storage/storage_ensure_file_shares_soft_delete_is_enabled/storage_ensure_file_shares_soft_delete_is_enabled.metadata.json b/prowler/providers/azure/services/storage/storage_ensure_file_shares_soft_delete_is_enabled/storage_ensure_file_shares_soft_delete_is_enabled.metadata.json index 6746a0c63d..efdf786e0b 100644 --- a/prowler/providers/azure/services/storage/storage_ensure_file_shares_soft_delete_is_enabled/storage_ensure_file_shares_soft_delete_is_enabled.metadata.json +++ b/prowler/providers/azure/services/storage/storage_ensure_file_shares_soft_delete_is_enabled/storage_ensure_file_shares_soft_delete_is_enabled.metadata.json @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "", + "ResourceType": "AzureStorageAccount", "Description": "Ensure that soft delete is enabled for Azure File Shares to protect against accidental or malicious deletion of important data. This feature allows deleted file shares to be retained for a specified period, during which they can be recovered before permanent deletion occurs.", "Risk": "Without soft delete enabled, accidental or malicious deletions of file shares result in permanent data loss, making recovery impossible unless a separate backup mechanism is in place.", "RelatedUrl": "https://learn.microsoft.com/en-us/azure/storage/files/storage-files-prevent-file-share-deletion?tabs=azure-portal", diff --git a/prowler/providers/gcp/services/compute/compute_project_os_login_enabled/compute_project_os_login_enabled.metadata.json b/prowler/providers/gcp/services/compute/compute_project_os_login_enabled/compute_project_os_login_enabled.metadata.json index 4e6edde752..1362070efe 100644 --- a/prowler/providers/gcp/services/compute/compute_project_os_login_enabled/compute_project_os_login_enabled.metadata.json +++ b/prowler/providers/gcp/services/compute/compute_project_os_login_enabled/compute_project_os_login_enabled.metadata.json @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "low", - "ResourceType": "Project", + "ResourceType": "GCPProject", "Description": "Ensure that the OS Login feature is enabled at the Google Cloud Platform (GCP) project level in order to provide you with centralized and automated SSH key pair management.", "Risk": "Enabling OS Login feature ensures that the SSH keys used to connect to VM instances are mapped with Google Cloud IAM users. Revoking access to corresponding IAM users will revoke all the SSH keys associated with these users, therefore it facilitates centralized SSH key pair management, which is extremely useful in handling compromised or stolen SSH key pairs and/or revocation of external/third-party/vendor users.", "RelatedUrl": "", diff --git a/prowler/providers/gcp/services/iam/iam_audit_logs_enabled/iam_audit_logs_enabled.metadata.json b/prowler/providers/gcp/services/iam/iam_audit_logs_enabled/iam_audit_logs_enabled.metadata.json index e3d448c257..3e7589c783 100644 --- a/prowler/providers/gcp/services/iam/iam_audit_logs_enabled/iam_audit_logs_enabled.metadata.json +++ b/prowler/providers/gcp/services/iam/iam_audit_logs_enabled/iam_audit_logs_enabled.metadata.json @@ -7,7 +7,7 @@ "SubServiceName": "Audit Logs", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "", + "ResourceType": "GCPProject", "Description": "Ensure that Google Cloud Audit Logs feature is configured to track Data Access logs for all Google Cloud Platform (GCP) services and users, in order to enhance overall access security and meet compliance requirements. Once configured, the feature can record all admin related activities, as well as all the read and write access requests to user data.", "Risk": "In order to maintain an effective Google Cloud audit configuration for your project, folder, and organization, all 3 types of Data Access logs (ADMIN_READ, DATA_READ and DATA_WRITE) must be enabled for all supported GCP services. Also, Data Access logs should be captured for all IAM users, without exempting any of them. Exemptions let you control which users generate audit logs. When you add an exempted user to your log configuration, audit logs are not created for that user, for the selected log type(s). Data Access audit logs are disabled by default and must be explicitly enabled based on your business requirements.", "RelatedUrl": "", diff --git a/prowler/providers/github/services/repository/repository_secret_scanning_enabled/__init__.py b/prowler/providers/github/services/repository/repository_secret_scanning_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/lib/check/check_loader_test.py b/tests/lib/check/check_loader_test.py index 06c1aa1c16..69ea5715f3 100644 --- a/tests/lib/check/check_loader_test.py +++ b/tests/lib/check/check_loader_test.py @@ -180,6 +180,9 @@ class TestCheckLoader: def test_load_checks_to_execute_with_compliance_frameworks( self, ): + bulk_checks_metatada = { + S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME: self.get_custom_check_s3_metadata() + } bulk_compliance_frameworks = { "soc2_aws": Compliance( Framework="SOC2", @@ -199,6 +202,7 @@ class TestCheckLoader: compliance_frameworks = ["soc2_aws"] assert {S3_BUCKET_LEVEL_PUBLIC_ACCESS_BLOCK_NAME} == load_checks_to_execute( + bulk_checks_metadata=bulk_checks_metatada, bulk_compliance_frameworks=bulk_compliance_frameworks, compliance_frameworks=compliance_frameworks, provider=self.provider, diff --git a/tests/lib/check/models_test.py b/tests/lib/check/models_test.py index fdbb8b6d70..70be6795ca 100644 --- a/tests/lib/check/models_test.py +++ b/tests/lib/check/models_test.py @@ -304,13 +304,14 @@ class TestCheckMetada: # Assertions assert result == {"accessanalyzer_enabled"} - def test_list_by_compliance_empty(self): + @mock.patch("prowler.lib.check.models.CheckMetadata.get_bulk") + def test_list_by_compliance_empty(self, mock_get_bulk): + mock_get_bulk.return_value = {} bulk_compliance_frameworks = custom_compliance_metadata result = CheckMetadata.list( bulk_compliance_frameworks=bulk_compliance_frameworks, compliance_framework="framework1_azure", ) - # Assertions assert result == set() diff --git a/tests/lib/outputs/finding_test.py b/tests/lib/outputs/finding_test.py index 3dea2f614b..ca3eef5ee0 100644 --- a/tests/lib/outputs/finding_test.py +++ b/tests/lib/outputs/finding_test.py @@ -28,7 +28,7 @@ def mock_check_metadata(provider): SubServiceName="", ResourceIdTemplate="", Severity="high", - ResourceType="", + ResourceType="mock_resource_type", Description="", Risk="", RelatedUrl="", @@ -209,7 +209,7 @@ class TestFinding: assert finding_output.metadata.SubServiceName == "" assert finding_output.metadata.ResourceIdTemplate == "" assert finding_output.metadata.Severity == Severity.high - assert finding_output.metadata.ResourceType == "" + assert finding_output.metadata.ResourceType == "mock_resource_type" assert finding_output.metadata.Description == "" assert finding_output.metadata.Risk == "" assert finding_output.metadata.RelatedUrl == "" @@ -230,7 +230,7 @@ class TestFinding: assert finding_output.check_id == "mock_check_id" assert finding_output.severity == Severity.high.value assert finding_output.status == Status.PASS.value - assert finding_output.resource_type == "" + assert finding_output.resource_type == "mock_resource_type" assert finding_output.service_name == "mock_service_name" assert finding_output.raw == {} @@ -310,7 +310,7 @@ class TestFinding: assert finding_output.metadata.SubServiceName == "" assert finding_output.metadata.ResourceIdTemplate == "" assert finding_output.metadata.Severity == Severity.high - assert finding_output.metadata.ResourceType == "" + assert finding_output.metadata.ResourceType == "mock_resource_type" assert finding_output.metadata.Description == "" assert finding_output.metadata.Risk == "" assert finding_output.metadata.RelatedUrl == "" @@ -405,7 +405,7 @@ class TestFinding: assert finding_output.metadata.SubServiceName == "" assert finding_output.metadata.ResourceIdTemplate == "" assert finding_output.metadata.Severity == Severity.high - assert finding_output.metadata.ResourceType == "" + assert finding_output.metadata.ResourceType == "mock_resource_type" assert finding_output.metadata.Description == "" assert finding_output.metadata.Risk == "" assert finding_output.metadata.RelatedUrl == "" @@ -490,7 +490,7 @@ class TestFinding: assert finding_output.metadata.SubServiceName == "" assert finding_output.metadata.ResourceIdTemplate == "" assert finding_output.metadata.Severity == Severity.high - assert finding_output.metadata.ResourceType == "" + assert finding_output.metadata.ResourceType == "mock_resource_type" assert finding_output.metadata.Description == "" assert finding_output.metadata.Risk == "" assert finding_output.metadata.RelatedUrl == "" diff --git a/tests/lib/scan/scan_test.py b/tests/lib/scan/scan_test.py index d51d26c3a3..1699902e12 100644 --- a/tests/lib/scan/scan_test.py +++ b/tests/lib/scan/scan_test.py @@ -117,6 +117,7 @@ def mock_load_check_metadata(): ) as mock_load: mock_metadata = MagicMock() mock_metadata.CheckID = "accessanalyzer_enabled" + mock_metadata.ResourceType = "AWS::IAM::AccessAnalyzer" mock_load.return_value = mock_metadata yield mock_load @@ -130,8 +131,22 @@ def mock_load_checks_to_execute(): yield mock_load +@pytest.fixture +def mock_check_metadata_get_bulk(): + with mock.patch( + "prowler.lib.check.models.CheckMetadata.get_bulk", autospec=True + ) as mock_get_bulk: + mock_metadata = MagicMock() + mock_metadata.CheckID = "accessanalyzer_enabled" + mock_metadata.ResourceType = "AWS::IAM::AccessAnalyzer" + mock_get_bulk.return_value = {"accessanalyzer_enabled": mock_metadata} + yield mock_get_bulk + + class TestScan: - def test_init(mock_provider): + def test_init( + mock_provider, + ): checks_to_execute = { "workspaces_vpc_2private_1public_subnets_nat", "workspaces_vpc_2private_1public_subnets_nat", @@ -194,102 +209,56 @@ class TestScan: "config_recorder_all_regions_enabled", } mock_provider.type = "aws" - scan = Scan(mock_provider, checks=checks_to_execute) + # Patch get_bulk to return all these checks + with mock.patch( + "prowler.lib.check.models.CheckMetadata.get_bulk" + ) as mock_get_bulk: + mock_metadata = MagicMock() + mock_metadata.ResourceType = "AWS::IAM::AccessAnalyzer" + mock_metadata.Categories = [] + mock_get_bulk.return_value = { + check: mock_metadata for check in checks_to_execute + } + scan = Scan(mock_provider, checks=checks_to_execute) - assert scan.provider == mock_provider - # Check that the checks to execute are sorted and without duplicates - assert scan.checks_to_execute == [ - "accessanalyzer_enabled", - "accessanalyzer_enabled_without_findings", - "account_maintain_current_contact_details", - "account_maintain_different_contact_details_to_security_billing_and_operations", - "account_security_contact_information_is_registered", - "account_security_questions_are_registered_in_the_aws_account", - "acm_certificates_expiration_check", - "acm_certificates_transparency_logs_enabled", - "apigateway_restapi_authorizers_enabled", - "apigateway_restapi_client_certificate_enabled", - "apigateway_restapi_logging_enabled", - "apigateway_restapi_public", - "awslambda_function_not_publicly_accessible", - "awslambda_function_url_cors_policy", - "awslambda_function_url_public", - "awslambda_function_using_supported_runtimes", - "backup_plans_exist", - "backup_reportplans_exist", - "backup_vaults_encrypted", - "backup_vaults_exist", - "cloudformation_stack_outputs_find_secrets", - "cloudformation_stacks_termination_protection_enabled", - "cloudwatch_cross_account_sharing_disabled", - "cloudwatch_log_group_kms_encryption_enabled", - "cloudwatch_log_group_no_secrets_in_logs", - "cloudwatch_log_group_retention_policy_specific_days_enabled", - "cloudwatch_log_metric_filter_and_alarm_for_aws_config_configuration_changes_enabled", - "cloudwatch_log_metric_filter_and_alarm_for_cloudtrail_configuration_changes_enabled", - "cloudwatch_log_metric_filter_authentication_failures", - "cloudwatch_log_metric_filter_aws_organizations_changes", - "cloudwatch_log_metric_filter_disable_or_scheduled_deletion_of_kms_cmk", - "cloudwatch_log_metric_filter_for_s3_bucket_policy_changes", - "cloudwatch_log_metric_filter_policy_changes", - "cloudwatch_log_metric_filter_root_usage", - "cloudwatch_log_metric_filter_security_group_changes", - "cloudwatch_log_metric_filter_sign_in_without_mfa", - "cloudwatch_log_metric_filter_unauthorized_api_calls", - "codeartifact_packages_external_public_publishing_disabled", - "codebuild_project_older_90_days", - "codebuild_project_user_controlled_buildspec", - "cognito_identity_pool_guest_access_disabled", - "cognito_user_pool_advanced_security_enabled", - "cognito_user_pool_blocks_compromised_credentials_sign_in_attempts", - "cognito_user_pool_blocks_potential_malicious_sign_in_attempts", - "cognito_user_pool_client_prevent_user_existence_errors", - "cognito_user_pool_client_token_revocation_enabled", - "cognito_user_pool_deletion_protection_enabled", - "cognito_user_pool_mfa_enabled", - "cognito_user_pool_password_policy_lowercase", - "cognito_user_pool_password_policy_minimum_length_14", - "cognito_user_pool_password_policy_number", - "cognito_user_pool_password_policy_symbol", - "cognito_user_pool_password_policy_uppercase", - "cognito_user_pool_self_registration_disabled", - "cognito_user_pool_temporary_password_expiration", - "cognito_user_pool_waf_acl_attached", - "config_recorder_all_regions_enabled", - "workspaces_vpc_2private_1public_subnets_nat", - ] - assert scan.service_checks_to_execute == get_service_checks_to_execute( - checks_to_execute - ) - assert scan.service_checks_completed == {} - assert scan.progress == 0 - assert scan.duration == 0 - assert scan.get_completed_services() == set() - assert scan.get_completed_checks() == set() + assert scan.provider == mock_provider + # Check that the checks to execute are sorted and without duplicates + assert scan.checks_to_execute == sorted(list(checks_to_execute)) + assert scan.service_checks_to_execute == get_service_checks_to_execute( + checks_to_execute + ) + assert scan.service_checks_completed == {} + assert scan.progress == 0 + assert scan.duration == 0 + assert scan.get_completed_services() == set() + assert scan.get_completed_checks() == set() def test_init_with_no_checks( mock_provider, mock_recover_checks_from_provider, mock_load_check_metadata, - mock_load_checks_to_execute, ): checks_to_execute = set() mock_provider.type = "aws" - - scan = Scan(mock_provider, checks=checks_to_execute) - mock_load_check_metadata.assert_called_once() - mock_load_checks_to_execute.assert_called_once() - mock_recover_checks_from_provider.assert_called_once_with("aws") - - assert scan.provider == mock_provider - assert scan.checks_to_execute == ["accessanalyzer_enabled"] - assert scan.service_checks_to_execute == get_service_checks_to_execute( - ["accessanalyzer_enabled"] - ) - assert scan.service_checks_completed == {} - assert scan.progress == 0 - assert scan.get_completed_services() == set() - assert scan.get_completed_checks() == set() + # Patch get_bulk to return only accessanalyzer_enabled + with mock.patch( + "prowler.lib.check.models.CheckMetadata.get_bulk" + ) as mock_get_bulk: + mock_metadata = MagicMock() + mock_metadata.ResourceType = "AWS::IAM::AccessAnalyzer" + mock_metadata.Categories = [] + mock_get_bulk.return_value = {"accessanalyzer_enabled": mock_metadata} + scan = Scan(mock_provider, checks=checks_to_execute) + # Remove assertion for mock_load_check_metadata + assert scan.provider == mock_provider + assert scan.checks_to_execute == ["accessanalyzer_enabled"] + assert scan.service_checks_to_execute == get_service_checks_to_execute( + ["accessanalyzer_enabled"] + ) + assert scan.service_checks_completed == {} + assert scan.progress == 0 + assert scan.get_completed_services() == set() + assert scan.get_completed_checks() == set() @patch("importlib.import_module") def test_scan(