feat(secretsmanager): add new check secretsmanager_secret_rotated_periodically (#5450)

Co-authored-by: Sergio <sergio@prowler.com>
This commit is contained in:
Rubén De la Torre Vico
2024-11-04 22:08:38 +01:00
committed by GitHub
parent 230d2571f9
commit 1b50fdba28
12 changed files with 333 additions and 9 deletions
+1
View File
@@ -59,6 +59,7 @@ The following list includes all the AWS checks with configurable variables that
| `rds_instance_backup_enabled` | `check_rds_instance_replicas` | Boolean |
| `securityhub_enabled` | `mute_non_default_regions` | Boolean |
| `secretsmanager_secret_unused` | `max_days_secret_unused` | Integer |
| `secretsmanager_secret_rotated_periodically` | `max_days_secret_unrotated` | 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 |
+4
View File
@@ -389,6 +389,10 @@ aws:
# Maximum number of days a secret can be unused
max_days_secret_unused: 90
# aws.secretsmanager_secret_rotated_periodically
# Maximum number of days a secret should be rotated
max_days_secret_unrotated: 90
# Azure Configuration
azure:
# Azure Network Configuration
@@ -0,0 +1,32 @@
{
"Provider": "aws",
"CheckID": "secretsmanager_secret_rotated_periodically",
"CheckTitle": "Secrets should be rotated periodically",
"CheckType": [],
"ServiceName": "secretsmanager",
"SubServiceName": "",
"ResourceIdTemplate": "arn:aws:secretsmanager:region:account-id:secret:secret-name",
"Severity": "medium",
"ResourceType": "AwsSecretsManagerSecret",
"Description": "Secrets should be rotated periodically to reduce the risk of unauthorized access.",
"Risk": "Rotating secrets in your AWS account reduces the risk of unauthorized access, especially for credentials like passwords or API keys. Automatic rotation via AWS Secrets Manager replaces long-term secrets with short-term ones, lowering the chances of compromise.",
"RelatedUrl": "https://docs.aws.amazon.com/secretsmanager/latest/userguide/rotating-secrets.html",
"Remediation": {
"Code": {
"CLI": "aws secretsmanager rotate-secret --secret-id <secret-name>",
"NativeIaC": "",
"Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/secretsmanager-controls.html#secretsmanager-4",
"Terraform": ""
},
"Recommendation": {
"Text": "Configure automatic rotation for your Secrets Manager secrets.",
"Url": "https://docs.aws.amazon.com/secretsmanager/latest/userguide/rotate-secrets_lambda.html"
}
},
"Categories": [
"secrets"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,52 @@
from datetime import datetime, timezone
from typing import List
from prowler.lib.check.models import Check, Check_Report_AWS
from prowler.providers.aws.services.secretsmanager.secretsmanager_client import (
secretsmanager_client,
)
class secretsmanager_secret_rotated_periodically(Check):
"""Check if AWS Secret Manager secrets are rotated periodically.
This class checks if each secret in AWS Secret Manager has been rotated periodically
the maximum number of days allowed could be configured in the audit_config file as max_days_secret_unrotated.
"""
def execute(self) -> List[Check_Report_AWS]:
"""Execute secretsmanager_secret_rotated_periodically check.
Iterate over all secrets in AWS Secret Manager and check if each secret has been rotated in the past
max_days_secret_unrotated days.
Returns:
List of reports objects for each secret in AWS Secret Manager.
"""
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} was last rotated on {secret.last_rotated_date.strftime('%B %d, %Y')}."
if secret.last_rotated_date == datetime.min.replace(tzinfo=timezone.utc):
report.status = "FAIL"
report.status_extended = f"Secret {secret.name} has never been rotated."
else:
days_since_last_rotation = (
datetime.now(timezone.utc) - secret.last_rotated_date
).days
if days_since_last_rotation > secretsmanager_client.audit_config.get(
"max_days_secret_unrotated", 90
):
report.status = "FAIL"
report.status_extended = f"Secret {secret.name} has not been rotated in {days_since_last_rotation} days, which is more than the maximum allowed of {secretsmanager_client.audit_config.get('max_days_secret_unrotated', 90)} days."
findings.append(report)
return findings
@@ -1,8 +1,8 @@
import json
from datetime import datetime, timezone
from typing import Optional
from typing import Dict, List, Optional
from pydantic import BaseModel
from pydantic import BaseModel, Field
from prowler.lib.logger import logger
from prowler.lib.scan_filters.scan_filters import is_resource_filtered
@@ -10,14 +10,25 @@ from prowler.providers.aws.lib.service.service import AWSService
class SecretsManager(AWSService):
"""AWS Secrets Manager service class to list secrets."""
def __init__(self, provider):
# Call AWSService's __init__
"""Initialize SecretsManager service.
Args:
provider: The AWS provider instance.
"""
super().__init__(__class__.__name__, provider)
self.secrets = {}
self.__threading_call__(self._list_secrets)
self.__threading_call__(self._get_resource_policy, self.secrets.values())
def _list_secrets(self, regional_client):
"""List all secrets in the region.
Args:
regional_client: The regional AWS client to list secrets.
"""
logger.info("SecretsManager - Listing Secrets...")
try:
list_secrets_paginator = regional_client.get_paginator("list_secrets")
@@ -31,15 +42,15 @@ class SecretsManager(AWSService):
arn=secret["ARN"],
name=secret["Name"],
region=regional_client.region,
rotation_enabled=secret.get("RotationEnabled", False),
last_rotated_date=secret.get(
"LastRotatedDate", datetime.min
).replace(tzinfo=timezone.utc),
last_accessed_date=secret.get(
"LastAccessedDate", datetime.min
).replace(tzinfo=timezone.utc),
tags=secret.get("Tags"),
)
if "RotationEnabled" in secret:
self.secrets[secret["ARN"]].rotation_enabled = secret[
"RotationEnabled"
]
except Exception as error:
logger.error(
@@ -70,5 +81,6 @@ class Secret(BaseModel):
region: str
policy: Optional[dict] = None
rotation_enabled: bool = False
last_rotated_date: datetime
last_accessed_date: datetime
tags: Optional[list] = []
tags: Optional[List[Dict[str, str]]] = Field(default_factory=list)
+1
View File
@@ -331,6 +331,7 @@ config_aws = {
"elbv2_min_azs": 2,
"secrets_ignore_patterns": [],
"max_days_secret_unused": 90,
"max_days_secret_unrotated": 90,
}
config_azure = {
+4
View File
@@ -385,6 +385,10 @@ aws:
# Maximum number of days a secret can be unused
max_days_secret_unused: 90
# aws.secretsmanager_secret_rotated_periodically
# Maximum number of days a secret should be rotated
max_days_secret_unrotated: 90
# Azure Configuration
azure:
# Azure Network Configuration
@@ -38,6 +38,7 @@ class Test_secretsmanager_automatic_rotation_enabled:
name=secret_name,
rotation_enabled=False,
last_accessed_date=datetime.min,
last_rotated_date=datetime.min,
)
}
with mock.patch(
@@ -76,6 +77,7 @@ class Test_secretsmanager_automatic_rotation_enabled:
name=secret_name,
rotation_enabled=True,
last_accessed_date=datetime.min,
last_rotated_date=datetime.min,
)
}
with mock.patch(
@@ -0,0 +1,205 @@
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_rotated_recently(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, 1, 1, 0, 0, 0, tzinfo=timezone.utc
),
"LastRotatedDate": 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)
def mock_make_api_call_secret_not_rotated_for_99_days(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, 1, 1, 0, 0, 0, tzinfo=timezone.utc
),
"LastRotatedDate": datetime(
2023, 1, 1, 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_rotated_periodically:
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_rotated_periodically.secretsmanager_secret_rotated_periodically.secretsmanager_client",
new=SecretsManager(aws_provider),
):
# Test Check
from prowler.providers.aws.services.secretsmanager.secretsmanager_secret_rotated_periodically.secretsmanager_secret_rotated_periodically import (
secretsmanager_secret_rotated_periodically,
)
check = secretsmanager_secret_rotated_periodically()
result = check.execute()
assert len(result) == 0
@mock_aws
def test_secret_never_rotated(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_rotated_periodically.secretsmanager_secret_rotated_periodically.secretsmanager_client",
new=SecretsManager(aws_provider),
):
from prowler.providers.aws.services.secretsmanager.secretsmanager_secret_rotated_periodically.secretsmanager_secret_rotated_periodically import (
secretsmanager_secret_rotated_periodically,
)
check = secretsmanager_secret_rotated_periodically()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "Secret test-secret has never been rotated."
)
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_not_rotated_for_99_days,
)
@mock_aws
def test_secret_not_rotated_for_99_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_rotated_periodically.secretsmanager_secret_rotated_periodically.secretsmanager_client",
new=SecretsManager(aws_provider),
):
from prowler.providers.aws.services.secretsmanager.secretsmanager_secret_rotated_periodically.secretsmanager_secret_rotated_periodically import (
secretsmanager_secret_rotated_periodically,
)
check = secretsmanager_secret_rotated_periodically()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "Secret test-secret has not been rotated in 99 days, which is more than the maximum allowed of 90 days."
)
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"}]
@freeze_time("2023-04-10")
@patch(
"botocore.client.BaseClient._make_api_call",
new=mock_make_api_call_secret_rotated_recently,
)
@mock_aws
def test_secret_rotated_recently(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_rotated_periodically.secretsmanager_secret_rotated_periodically.secretsmanager_client",
new=SecretsManager(aws_provider),
):
from prowler.providers.aws.services.secretsmanager.secretsmanager_secret_rotated_periodically.secretsmanager_secret_rotated_periodically import (
secretsmanager_secret_rotated_periodically,
)
check = secretsmanager_secret_rotated_periodically()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== "Secret test-secret was last rotated 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"}]
@@ -21,6 +21,9 @@ def mock_make_api_call_secret_accessed_100_days_ago(self, operation_name, kwarg)
"LastAccessedDate": datetime(
2023, 1, 1, 0, 0, 0, tzinfo=timezone.utc
),
"LastRotatedDate": datetime(
2023, 4, 9, 0, 0, 0, tzinfo=timezone.utc
),
"Tags": [{"Key": "Name", "Value": "test-100-days-secret"}],
}
]
@@ -39,6 +42,9 @@ def mock_make_api_call_secret_accessed_yesterday(self, operation_name, kwarg):
"LastAccessedDate": datetime(
2023, 4, 9, 0, 0, 0, tzinfo=timezone.utc
),
"LastRotatedDate": datetime(
2023, 4, 9, 0, 0, 0, tzinfo=timezone.utc
),
"Tags": [{"Key": "Name", "Value": "test-secret"}],
}
]
@@ -4,6 +4,7 @@ from datetime import datetime, timezone
from unittest.mock import patch
from boto3 import client, resource
from freezegun import freeze_time
from moto import mock_aws
from prowler.providers.aws.services.secretsmanager.secretsmanager_service import (
@@ -51,6 +52,7 @@ class Test_SecretsManager_Service:
secretsmanager = SecretsManager(aws_provider)
assert secretsmanager.service == "secretsmanager"
@freeze_time("2023-04-09")
@mock_aws
def test_list_secrets(self):
secretsmanager_client = client(
@@ -126,7 +128,6 @@ class Test_SecretsManager_Service:
# Set partition for the service
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
secretsmanager = SecretsManager(aws_provider)
assert len(secretsmanager.secrets) == 1
assert secretsmanager.secrets
assert secretsmanager.secrets[secret_arn]
@@ -137,6 +138,10 @@ class Test_SecretsManager_Service:
assert secretsmanager.secrets[
secret_arn
].last_accessed_date == datetime.min.replace(tzinfo=timezone.utc)
assert (
secretsmanager.secrets[secret_arn].last_rotated_date.date()
== datetime(2023, 4, 9).date()
)
assert secretsmanager.secrets[secret_arn].tags == [
{"Key": "test", "Value": "test"},
]