mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-24 21:11:53 +00:00
feat(secretsmanager): add new check secretsmanager_secret_rotated_periodically (#5450)
Co-authored-by: Sergio <sergio@prowler.com>
This commit is contained in:
committed by
GitHub
parent
230d2571f9
commit
1b50fdba28
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
|
||||
+32
@@ -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": ""
|
||||
}
|
||||
+52
@@ -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)
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
|
||||
|
||||
+2
@@ -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(
|
||||
|
||||
+205
@@ -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"}]
|
||||
+6
@@ -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"},
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user