Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(secretsmanager): add new check secretsmanager_secret_unused #5428

Merged
merged 8 commits into from
Oct 17, 2024
Merged
1 change: 1 addition & 0 deletions docs/tutorials/configuration_file.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
5 changes: 5 additions & 0 deletions prowler/config/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <secret-arn>",
"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": ""
}
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from datetime import datetime, timezone
from typing import Optional

from pydantic import BaseModel
Expand All @@ -7,7 +8,6 @@
from prowler.providers.aws.lib.service.service import AWSService


################## SecretsManager
class SecretsManager(AWSService):
def __init__(self, provider):
# Call AWSService's __init__
Expand All @@ -29,6 +29,9 @@ def _list_secrets(self, regional_client):
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:
Expand All @@ -49,4 +52,5 @@ class Secret(BaseModel):
name: str
region: str
rotation_enabled: bool = False
last_accessed_date: datetime
tags: Optional[list] = []
1 change: 1 addition & 0 deletions tests/config/config_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,7 @@ def mock_prowler_get_latest_release(_, **kwargs):
"elb_min_azs": 2,
"elbv2_min_azs": 2,
"secrets_ignore_patterns": [],
"max_days_secret_unused": 90,
}

config_azure = {
Expand Down
5 changes: 5 additions & 0 deletions tests/config/fixtures/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from datetime import datetime
from unittest import mock

from prowler.providers.aws.services.secretsmanager.secretsmanager_service import Secret
Expand All @@ -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 (
Expand All @@ -32,11 +37,15 @@ def test_secret_rotation_disabled(self):
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 (
Expand Down Expand Up @@ -66,11 +75,15 @@ def test_secret_rotation_enabled(self):
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 (
Expand Down
Original file line number Diff line number Diff line change
@@ -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"}]
Loading