diff --git a/docs/tutorials/configuration_file.md b/docs/tutorials/configuration_file.md index ea4cd8dc02..ebe73803db 100644 --- a/docs/tutorials/configuration_file.md +++ b/docs/tutorials/configuration_file.md @@ -56,6 +56,7 @@ The following list includes all the AWS checks with configurable variables that | `organizations_scp_check_deny_regions` | `organizations_enabled_regions` | List of Strings | | `rds_instance_backup_enabled` | `check_rds_instance_replicas` | Boolean | | `securityhub_enabled` | `mute_non_default_regions` | Boolean | +| `secretsmanager_secret_unused` | `max_days_secret_unused` | Integer | | `ssm_document_secrets` | `secrets_ignore_patterns` | List of Strings | | `trustedadvisor_premium_support_plan_subscribed` | `verify_premium_support_plans` | Boolean | | `vpc_endpoint_connections_trust_boundaries` | `trusted_account_ids` | List of Strings | diff --git a/prowler/config/config.yaml b/prowler/config/config.yaml index 044e52947c..dacceaac1e 100644 --- a/prowler/config/config.yaml +++ b/prowler/config/config.yaml @@ -359,6 +359,11 @@ aws: # Patterns to ignore in the secrets checks secrets_ignore_patterns: [] + # AWS Secrets Manager Configuration + # aws.secretsmanager_secret_unused + # Maximum number of days a secret can be unused + max_days_secret_unused: 90 + # Azure Configuration azure: # Azure Network Configuration diff --git a/prowler/providers/aws/services/secretsmanager/secretsmanager_secret_unused/__init__.py b/prowler/providers/aws/services/secretsmanager/secretsmanager_secret_unused/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/secretsmanager/secretsmanager_secret_unused/secretsmanager_secret_unused.metadata.json b/prowler/providers/aws/services/secretsmanager/secretsmanager_secret_unused/secretsmanager_secret_unused.metadata.json new file mode 100644 index 0000000000..194d30158e --- /dev/null +++ b/prowler/providers/aws/services/secretsmanager/secretsmanager_secret_unused/secretsmanager_secret_unused.metadata.json @@ -0,0 +1,32 @@ +{ + "Provider": "aws", + "CheckID": "secretsmanager_secret_unused", + "CheckTitle": "Ensure secrets manager secrets are not unused", + "CheckType": [], + "ServiceName": "secretsmanager", + "SubServiceName": "", + "ResourceIdTemplate": "arn:aws:secretsmanager:region:account-id:secret:secret-name", + "Severity": "medium", + "ResourceType": "AwsSecretsManagerSecret", + "Description": "Checks whether Secrets Manager secrets are unused.", + "Risk": "Unused secrets can be abused by former users or leaked to unauthorized entities, increasing the risk of unauthorized access and data breaches.", + "RelatedUrl": "https://docs.aws.amazon.com/secretsmanager/latest/userguide/manage_delete-secret.html", + "Remediation": { + "Code": { + "CLI": "aws secretsmanager delete-secret --secret-id ", + "NativeIaC": "", + "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/secretsmanager-controls.html#secretsmanager-3", + "Terraform": "" + }, + "Recommendation": { + "Text": "Regularly review Secrets Manager secrets and delete those that are no longer in use.", + "Url": "https://docs.aws.amazon.com/secretsmanager/latest/userguide/manage_delete-secret.html" + } + }, + "Categories": [ + "secrets" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/secretsmanager/secretsmanager_secret_unused/secretsmanager_secret_unused.py b/prowler/providers/aws/services/secretsmanager/secretsmanager_secret_unused/secretsmanager_secret_unused.py new file mode 100644 index 0000000000..a1b9d8b5b7 --- /dev/null +++ b/prowler/providers/aws/services/secretsmanager/secretsmanager_secret_unused/secretsmanager_secret_unused.py @@ -0,0 +1,38 @@ +from datetime import datetime, timedelta, timezone + +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.secretsmanager.secretsmanager_client import ( + secretsmanager_client, +) + + +class secretsmanager_secret_unused(Check): + def execute(self): + findings = [] + for secret in secretsmanager_client.secrets.values(): + report = Check_Report_AWS(self.metadata()) + report.resource_id = secret.name + report.resource_arn = secret.arn + report.region = secret.region + report.resource_tags = secret.tags + report.status = "PASS" + report.status_extended = f"Secret {secret.name} has been accessed recently, last accessed on {secret.last_accessed_date.strftime('%B %d, %Y')}." + + if (datetime.now(timezone.utc) - secret.last_accessed_date) > timedelta( + days=secretsmanager_client.audit_config.get( + "max_days_secret_unused", 90 + ) + ): + report.status = "FAIL" + if secret.last_accessed_date == datetime.min.replace( + tzinfo=timezone.utc + ): + report.status_extended = ( + f"Secret {secret.name} has never been accessed." + ) + else: + report.status_extended = f"Secret {secret.name} has not been accessed since {secret.last_accessed_date.strftime('%B %d, %Y')}, you should review if it is still needed." + + findings.append(report) + + return findings diff --git a/prowler/providers/aws/services/secretsmanager/secretsmanager_service.py b/prowler/providers/aws/services/secretsmanager/secretsmanager_service.py index 2bd4096479..f84822d9ae 100644 --- a/prowler/providers/aws/services/secretsmanager/secretsmanager_service.py +++ b/prowler/providers/aws/services/secretsmanager/secretsmanager_service.py @@ -1,3 +1,4 @@ +from datetime import datetime, timezone from typing import Optional from pydantic import BaseModel @@ -7,7 +8,6 @@ from prowler.lib.scan_filters.scan_filters import is_resource_filtered from prowler.providers.aws.lib.service.service import AWSService -################## SecretsManager class SecretsManager(AWSService): def __init__(self, provider): # Call AWSService's __init__ @@ -29,6 +29,9 @@ class SecretsManager(AWSService): arn=secret["ARN"], name=secret["Name"], region=regional_client.region, + last_accessed_date=secret.get( + "LastAccessedDate", datetime.min + ).replace(tzinfo=timezone.utc), tags=secret.get("Tags"), ) if "RotationEnabled" in secret: @@ -49,4 +52,5 @@ class Secret(BaseModel): name: str region: str rotation_enabled: bool = False + last_accessed_date: datetime tags: Optional[list] = [] diff --git a/tests/config/config_test.py b/tests/config/config_test.py index d1519878df..5989b92568 100644 --- a/tests/config/config_test.py +++ b/tests/config/config_test.py @@ -310,6 +310,7 @@ config_aws = { "elb_min_azs": 2, "elbv2_min_azs": 2, "secrets_ignore_patterns": [], + "max_days_secret_unused": 90, } config_azure = { diff --git a/tests/config/fixtures/config.yaml b/tests/config/fixtures/config.yaml index 74f82b6c66..c2ea3a0ff6 100644 --- a/tests/config/fixtures/config.yaml +++ b/tests/config/fixtures/config.yaml @@ -355,6 +355,11 @@ aws: # Patterns to ignore in the secrets checks secrets_ignore_patterns: [] + # AWS Secrets Manager Configuration + # aws.secretsmanager_secret_unused + # Maximum number of days a secret can be unused + max_days_secret_unused: 90 + # Azure Configuration azure: # Azure Network Configuration diff --git a/tests/providers/aws/services/secretsmanager/secretsmanager_automatic_rotation_enabled/secretsmanager_automatic_rotation_enabled_test.py b/tests/providers/aws/services/secretsmanager/secretsmanager_automatic_rotation_enabled/secretsmanager_automatic_rotation_enabled_test.py index 3134106e5c..d863a26a80 100644 --- a/tests/providers/aws/services/secretsmanager/secretsmanager_automatic_rotation_enabled/secretsmanager_automatic_rotation_enabled_test.py +++ b/tests/providers/aws/services/secretsmanager/secretsmanager_automatic_rotation_enabled/secretsmanager_automatic_rotation_enabled_test.py @@ -1,3 +1,4 @@ +from datetime import datetime from unittest import mock from prowler.providers.aws.services.secretsmanager.secretsmanager_service import Secret @@ -8,9 +9,13 @@ class Test_secretsmanager_automatic_rotation_enabled: def test_no_secrets(self): secretsmanager_client = mock.MagicMock secretsmanager_client.secrets = {} + with mock.patch( "prowler.providers.aws.services.secretsmanager.secretsmanager_service.SecretsManager", new=secretsmanager_client, + ), mock.patch( + "prowler.providers.aws.services.secretsmanager.secretsmanager_client.secretsmanager_client", + new=secretsmanager_client, ): # Test Check from prowler.providers.aws.services.secretsmanager.secretsmanager_automatic_rotation_enabled.secretsmanager_automatic_rotation_enabled import ( @@ -32,11 +37,15 @@ class Test_secretsmanager_automatic_rotation_enabled: region=AWS_REGION_EU_WEST_1, name=secret_name, rotation_enabled=False, + last_accessed_date=datetime.min, ) } with mock.patch( "prowler.providers.aws.services.secretsmanager.secretsmanager_service.SecretsManager", new=secretsmanager_client, + ), mock.patch( + "prowler.providers.aws.services.secretsmanager.secretsmanager_client.secretsmanager_client", + new=secretsmanager_client, ): # Test Check from prowler.providers.aws.services.secretsmanager.secretsmanager_automatic_rotation_enabled.secretsmanager_automatic_rotation_enabled import ( @@ -66,11 +75,15 @@ class Test_secretsmanager_automatic_rotation_enabled: region=AWS_REGION_EU_WEST_1, name=secret_name, rotation_enabled=True, + last_accessed_date=datetime.min, ) } with mock.patch( "prowler.providers.aws.services.secretsmanager.secretsmanager_service.SecretsManager", new=secretsmanager_client, + ), mock.patch( + "prowler.providers.aws.services.secretsmanager.secretsmanager_client.secretsmanager_client", + new=secretsmanager_client, ): # Test Check from prowler.providers.aws.services.secretsmanager.secretsmanager_automatic_rotation_enabled.secretsmanager_automatic_rotation_enabled import ( diff --git a/tests/providers/aws/services/secretsmanager/secretsmanager_secret_unused/secretsmanager_secret_unused_test.py b/tests/providers/aws/services/secretsmanager/secretsmanager_secret_unused/secretsmanager_secret_unused_test.py new file mode 100644 index 0000000000..1e41857d4c --- /dev/null +++ b/tests/providers/aws/services/secretsmanager/secretsmanager_secret_unused/secretsmanager_secret_unused_test.py @@ -0,0 +1,200 @@ +from datetime import datetime, timezone +from unittest.mock import patch + +import botocore +from boto3 import client +from freezegun import freeze_time +from moto import mock_aws + +from tests.providers.aws.utils import AWS_REGION_EU_WEST_1, set_mocked_aws_provider + +orig = botocore.client.BaseClient._make_api_call + + +def mock_make_api_call_secret_accessed_100_days_ago(self, operation_name, kwarg): + if operation_name == "ListSecrets": + return { + "SecretList": [ + { + "ARN": "arn:aws:secretsmanager:eu-west-1:123456789012:secret:test-100-days-secret", + "Name": "test-100-days-secret", + "LastAccessedDate": datetime( + 2023, 1, 1, 0, 0, 0, tzinfo=timezone.utc + ), + "Tags": [{"Key": "Name", "Value": "test-100-days-secret"}], + } + ] + } + # If we don't want to patch the API call + return orig(self, operation_name, kwarg) + + +def mock_make_api_call_secret_accessed_yesterday(self, operation_name, kwarg): + if operation_name == "ListSecrets": + return { + "SecretList": [ + { + "ARN": "arn:aws:secretsmanager:eu-west-1:123456789012:secret:test-secret", + "Name": "test-secret", + "LastAccessedDate": datetime( + 2023, 4, 9, 0, 0, 0, tzinfo=timezone.utc + ), + "Tags": [{"Key": "Name", "Value": "test-secret"}], + } + ] + } + # If we don't want to patch the API call + return orig(self, operation_name, kwarg) + + +class Test_secretsmanager_secret_unused: + def test_no_secrets(self): + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + from prowler.providers.aws.services.secretsmanager.secretsmanager_service import ( + SecretsManager, + ) + + with patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), patch( + "prowler.providers.aws.services.secretsmanager.secretsmanager_secret_unused.secretsmanager_secret_unused.secretsmanager_client", + new=SecretsManager(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.secretsmanager.secretsmanager_secret_unused.secretsmanager_secret_unused import ( + secretsmanager_secret_unused, + ) + + check = secretsmanager_secret_unused() + result = check.execute() + + assert len(result) == 0 + + @mock_aws + def test_secret_never_used(self): + secretsmanager_client = client( + "secretsmanager", region_name=AWS_REGION_EU_WEST_1 + ) + + secret_arn = secretsmanager_client.create_secret( + Name="test-secret", + Tags=[ + {"Key": "Name", "Value": "test-secret"}, + ], + )["ARN"] + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + from prowler.providers.aws.services.secretsmanager.secretsmanager_service import ( + SecretsManager, + ) + + with patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), patch( + "prowler.providers.aws.services.secretsmanager.secretsmanager_secret_unused.secretsmanager_secret_unused.secretsmanager_client", + new=SecretsManager(aws_provider), + ): + from prowler.providers.aws.services.secretsmanager.secretsmanager_secret_unused.secretsmanager_secret_unused import ( + secretsmanager_secret_unused, + ) + + check = secretsmanager_secret_unused() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Secret test-secret has never been accessed." + ) + assert result[0].resource_id == "test-secret" + assert result[0].resource_arn == secret_arn + assert result[0].region == AWS_REGION_EU_WEST_1 + assert result[0].resource_tags == [{"Key": "Name", "Value": "test-secret"}] + + @freeze_time("2023-04-10") + @patch( + "botocore.client.BaseClient._make_api_call", + new=mock_make_api_call_secret_accessed_100_days_ago, + ) + @mock_aws + def test_secret_unused_for_last_100_days(self): + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + from prowler.providers.aws.services.secretsmanager.secretsmanager_service import ( + SecretsManager, + ) + + with patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), patch( + "prowler.providers.aws.services.secretsmanager.secretsmanager_secret_unused.secretsmanager_secret_unused.secretsmanager_client", + new=SecretsManager(aws_provider), + ): + from prowler.providers.aws.services.secretsmanager.secretsmanager_secret_unused.secretsmanager_secret_unused import ( + secretsmanager_secret_unused, + ) + + check = secretsmanager_secret_unused() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Secret test-100-days-secret has not been accessed since January 01, 2023, you should review if it is still needed." + ) + assert result[0].resource_id == "test-100-days-secret" + assert ( + result[0].resource_arn + == "arn:aws:secretsmanager:eu-west-1:123456789012:secret:test-100-days-secret" + ) + assert result[0].region == AWS_REGION_EU_WEST_1 + assert result[0].resource_tags == [ + {"Key": "Name", "Value": "test-100-days-secret"} + ] + + @freeze_time("2023-04-10") + @patch( + "botocore.client.BaseClient._make_api_call", + new=mock_make_api_call_secret_accessed_yesterday, + ) + @mock_aws + def test_secret_used_yesterday(self): + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + from prowler.providers.aws.services.secretsmanager.secretsmanager_service import ( + SecretsManager, + ) + + with patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), patch( + "prowler.providers.aws.services.secretsmanager.secretsmanager_secret_unused.secretsmanager_secret_unused.secretsmanager_client", + new=SecretsManager(aws_provider), + ): + from prowler.providers.aws.services.secretsmanager.secretsmanager_secret_unused.secretsmanager_secret_unused import ( + secretsmanager_secret_unused, + ) + + check = secretsmanager_secret_unused() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Secret test-secret has been accessed recently, last accessed on April 09, 2023." + ) + assert result[0].resource_id == "test-secret" + assert result[0].resource_arn == ( + "arn:aws:secretsmanager:eu-west-1:123456789012:secret:test-secret" + ) + assert result[0].region == AWS_REGION_EU_WEST_1 + assert result[0].resource_tags == [{"Key": "Name", "Value": "test-secret"}] diff --git a/tests/providers/aws/services/secretsmanager/secretsmanager_service_test.py b/tests/providers/aws/services/secretsmanager/secretsmanager_service_test.py index 62187735b5..33dd7ada91 100644 --- a/tests/providers/aws/services/secretsmanager/secretsmanager_service_test.py +++ b/tests/providers/aws/services/secretsmanager/secretsmanager_service_test.py @@ -1,5 +1,6 @@ import io import zipfile +from datetime import datetime, timezone from unittest.mock import patch from boto3 import client, resource @@ -133,6 +134,9 @@ class Test_SecretsManager_Service: assert secretsmanager.secrets[secret_arn].arn == secret_arn assert secretsmanager.secrets[secret_arn].region == AWS_REGION_EU_WEST_1 assert secretsmanager.secrets[secret_arn].rotation_enabled is True + assert secretsmanager.secrets[ + secret_arn + ].last_accessed_date == datetime.min.replace(tzinfo=timezone.utc) assert secretsmanager.secrets[secret_arn].tags == [ {"Key": "test", "Value": "test"}, ]