feat(azure): New check storage_ensure_soft_delete_is_enabled (#3334)

This commit is contained in:
Pedro Martín
2024-01-31 13:29:20 +01:00
committed by GitHub
parent 622bce9c52
commit 6e991107e7
16 changed files with 436 additions and 2 deletions

View File

@@ -0,0 +1,32 @@
{
"Provider": "azure",
"CheckID": "storage_ensure_soft_delete_is_enabled",
"CheckTitle": "Ensure Soft Delete is Enabled for Azure Containers and Blob Storage",
"CheckType": [],
"ServiceName": "storage",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "AzureStorageAccount",
"Description": "The Azure Storage blobs contain data like ePHI or Financial, which can be secret or personal. Data that is erroneously modified or deleted by an application or other storage account user will cause data loss or unavailability.",
"Risk": "Containers and Blob Storage data can be incorrectly deleted. An attacker/malicious user may do this deliberately in order to cause disruption. Deleting an Azure Storage blob causes immediate data loss. Enabling this configuration for Azure storage ensures that even if blobs/data were deleted from the storage account, Blobs/data objects are recoverable for a particular time which is set in the Retention policies ranging from 7 days to 365 days.",
"RelatedUrl": "https://learn.microsoft.com/en-us/azure/storage/blobs/soft-delete-blob-enable?tabs=azure-portal",
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "https://www.trendmicro.com/cloudoneconformity-staging/knowledge-base/azure/StorageAccounts/enable-soft-delete.html#",
"Terraform": ""
},
"Recommendation": {
"Text": "From the Azure home page, open the hamburger menu in the top left or click on the arrow pointing right with 'More services' underneath. 2. Select Storage. 3. Select Storage Accounts. 4. For each Storage Account, navigate to Data protection in the left scroll column. 5. Check soft delete for both blobs and containers. Set the retention period to a sufficient length for your organization",
"Url": "https://docs.microsoft.com/en-us/azure/storage/blobs/storage-blob-soft-delete"
}
},
"Categories": [
"encryption"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "Additional storage costs may be incurred as snapshots are retained."
}

View File

@@ -0,0 +1,27 @@
from prowler.lib.check.models import Check, Check_Report_Azure
from prowler.providers.azure.services.storage.storage_client import storage_client
class storage_ensure_soft_delete_is_enabled(Check):
def execute(self) -> Check_Report_Azure:
findings = []
for subscription, storage_accounts in storage_client.storage_accounts.items():
for storage_account in storage_accounts:
if storage_account.blob_properties:
report = Check_Report_Azure(self.metadata())
report.subscription = subscription
report.resource_name = storage_account.name
report.resource_id = storage_account.id
if getattr(
storage_account.blob_properties.container_delete_retention_policy,
"enabled",
False,
):
report.status = "PASS"
report.status_extended = f"Storage account {storage_account.name} from subscription {subscription} has soft delete enabled."
else:
report.status = "FAIL"
report.status_extended = f"Storage account {storage_account.name} from subscription {subscription} has soft delete disabled."
findings.append(report)
return findings

View File

@@ -1,8 +1,11 @@
from dataclasses import dataclass
from azure.mgmt.storage import StorageManagementClient
from azure.mgmt.storage.v2022_09_01.models import NetworkRuleSet
from azure.mgmt.storage.v2023_01_01.models import PrivateEndpointConnection
from azure.mgmt.storage.v2022_09_01.models import (
DeleteRetentionPolicy,
NetworkRuleSet,
PrivateEndpointConnection,
)
from prowler.lib.logger import logger
from prowler.providers.azure.lib.service.service import AzureService
@@ -13,6 +16,7 @@ class Storage(AzureService):
def __init__(self, audit_info):
super().__init__(StorageManagementClient, audit_info)
self.storage_accounts = self.__get_storage_accounts__()
self.__get_blob_properties__()
def __get_storage_accounts__(self):
logger.info("Storage - Getting storage accounts...")
@@ -22,10 +26,17 @@ class Storage(AzureService):
storage_accounts.update({subscription: []})
storage_accounts_list = client.storage_accounts.list()
for storage_account in storage_accounts_list:
parts = storage_account.id.split("/")
if "resourceGroups" in parts:
resouce_name_index = parts.index("resourceGroups") + 1
resouce_group_name = parts[resouce_name_index]
else:
resouce_group_name = None
storage_accounts[subscription].append(
Storage_Account(
id=storage_account.id,
name=storage_account.name,
resouce_group_name=resouce_group_name,
enable_https_traffic_only=storage_account.enable_https_traffic_only,
infrastructure_encryption=storage_account.encryption.require_infrastructure_encryption,
allow_blob_public_access=storage_account.allow_blob_public_access,
@@ -42,11 +53,42 @@ class Storage(AzureService):
)
return storage_accounts
def __get_blob_properties__(self):
logger.info("Storage - Getting blob properties...")
try:
for subscription, accounts in self.storage_accounts.items():
client = self.clients[subscription]
for account in accounts:
properties = client.blob_services.get_service_properties(
account.resouce_group_name, account.name
)
account.blob_properties = Blob_Properties(
id=properties.id,
name=properties.name,
type=properties.type,
default_service_version=properties.default_service_version,
container_delete_retention_policy=properties.container_delete_retention_policy,
)
except Exception as error:
logger.error(
f"Subscription name: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
@dataclass
class Blob_Properties:
id: str
name: str
type: str
default_service_version: str
container_delete_retention_policy: DeleteRetentionPolicy
@dataclass
class Storage_Account:
id: str
name: str
resouce_group_name: str
enable_https_traffic_only: bool
infrastructure_encryption: bool
allow_blob_public_access: bool
@@ -55,3 +97,4 @@ class Storage_Account:
minimum_tls_version: str
private_endpoint_connections: PrivateEndpointConnection
key_expiration_period_in_days: str
blob_properties: Blob_Properties = None

View File

@@ -54,6 +54,13 @@ expected_packages = [
name="prowler.providers.azure.services.storage.storage_ensure_private_endpoints_in_storage_accounts.storage_ensure_private_endpoints_in_storage_accounts",
ispkg=False,
),
ModuleInfo(
module_finder=FileFinder(
"/root_dir/prowler/providers/azure/services/storage/storage_ensure_soft_delete_is_enabled"
),
name="prowler.providers.azure.services.storage.storage_ensure_soft_delete_is_enabled.storage_ensure_soft_delete_is_enabled",
ispkg=False,
),
ModuleInfo(
module_finder=FileFinder("/root_dir/prowler/providers/azure/services/storage"),
name="prowler.providers.azure.services.storage.storage_ensure_encryption_with_customer_managed_keys",
@@ -96,6 +103,13 @@ def mock_list_modules(*_):
name="prowler.providers.azure.services.storage.storage_ensure_private_endpoints_in_storage_accounts.storage_ensure_private_endpoints_in_storage_accounts",
ispkg=False,
),
ModuleInfo(
module_finder=FileFinder(
"/root_dir/prowler/providers/azure/services/storage/storage_ensure_soft_delete_is_enabled"
),
name="prowler.providers.azure.services.storage.storage_ensure_soft_delete_is_enabled.storage_ensure_soft_delete_is_enabled",
ispkg=False,
),
ModuleInfo(
module_finder=FileFinder(
"/root_dir/prowler/providers/azure/services/storage"
@@ -483,6 +497,10 @@ class Test_Check:
"storage_ensure_private_endpoints_in_storage_accounts",
"/root_dir/prowler/providers/azure/services/storage/storage_ensure_private_endpoints_in_storage_accounts",
),
(
"storage_ensure_soft_delete_is_enabled",
"/root_dir/prowler/providers/azure/services/storage/storage_ensure_soft_delete_is_enabled",
),
(
"storage_ensure_encryption_with_customer_managed_keys",
"/root_dir/prowler/providers/azure/services/storage/storage_ensure_encryption_with_customer_managed_keys",

View File

@@ -32,6 +32,7 @@ class Test_storage_blob_public_access_level_is_disabled:
Storage_Account(
id=storage_account_id,
name=storage_account_name,
resouce_group_name=None,
enable_https_traffic_only=False,
infrastructure_encryption=False,
allow_blob_public_access=True,
@@ -73,6 +74,7 @@ class Test_storage_blob_public_access_level_is_disabled:
Storage_Account(
id=storage_account_id,
name=storage_account_name,
resouce_group_name=None,
enable_https_traffic_only=False,
infrastructure_encryption=False,
allow_blob_public_access=False,

View File

@@ -34,6 +34,7 @@ class Test_storage_default_network_access_rule_is_denied:
Storage_Account(
id=storage_account_id,
name=storage_account_name,
resouce_group_name=None,
enable_https_traffic_only=False,
infrastructure_encryption=False,
allow_blob_public_access=None,
@@ -75,6 +76,7 @@ class Test_storage_default_network_access_rule_is_denied:
Storage_Account(
id=storage_account_id,
name=storage_account_name,
resouce_group_name=None,
enable_https_traffic_only=False,
infrastructure_encryption=False,
allow_blob_public_access=None,

View File

@@ -34,6 +34,7 @@ class Test_storage_ensure_azure_services_are_trusted_to_access_is_enabled:
Storage_Account(
id=storage_account_id,
name=storage_account_name,
resouce_group_name=None,
enable_https_traffic_only=False,
infrastructure_encryption=False,
allow_blob_public_access=None,
@@ -75,6 +76,7 @@ class Test_storage_ensure_azure_services_are_trusted_to_access_is_enabled:
Storage_Account(
id=storage_account_id,
name=storage_account_name,
resouce_group_name=None,
enable_https_traffic_only=False,
infrastructure_encryption=False,
allow_blob_public_access=None,

View File

@@ -32,6 +32,7 @@ class Test_storage_ensure_encryption_with_customer_managed_keys:
Storage_Account(
id=storage_account_id,
name=storage_account_name,
resouce_group_name=None,
enable_https_traffic_only=False,
infrastructure_encryption=False,
allow_blob_public_access=None,
@@ -73,6 +74,7 @@ class Test_storage_ensure_encryption_with_customer_managed_keys:
Storage_Account(
id=storage_account_id,
name=storage_account_name,
resouce_group_name=None,
enable_https_traffic_only=False,
infrastructure_encryption=False,
allow_blob_public_access=None,

View File

@@ -32,6 +32,7 @@ class Test_storage_ensure_minimum_tls_version_12:
Storage_Account(
id=storage_account_id,
name=storage_account_name,
resouce_group_name=None,
enable_https_traffic_only=False,
infrastructure_encryption=False,
allow_blob_public_access=None,
@@ -73,6 +74,7 @@ class Test_storage_ensure_minimum_tls_version_12:
Storage_Account(
id=storage_account_id,
name=storage_account_name,
resouce_group_name=None,
enable_https_traffic_only=False,
infrastructure_encryption=False,
allow_blob_public_access=None,

View File

@@ -36,6 +36,7 @@ class Test_storage_ensure_private_endpoints_in_storage_accounts:
Storage_Account(
id=storage_account_id,
name=storage_account_name,
resouce_group_name=None,
enable_https_traffic_only=False,
infrastructure_encryption=False,
allow_blob_public_access=None,
@@ -79,6 +80,7 @@ class Test_storage_ensure_private_endpoints_in_storage_accounts:
Storage_Account(
id=storage_account_id,
name=storage_account_name,
resouce_group_name=None,
enable_https_traffic_only=False,
infrastructure_encryption=False,
allow_blob_public_access=None,

View File

@@ -0,0 +1,169 @@
from unittest import mock
from uuid import uuid4
from azure.mgmt.storage.v2023_01_01.models import DeleteRetentionPolicy
from prowler.providers.azure.services.storage.storage_service import (
Blob_Properties,
Storage_Account,
)
AZURE_SUSCRIPTION = str(uuid4())
class Test_storage_ensure_soft_delete_is_enabled:
def test_storage_no_storage_accounts(self):
storage_client = mock.MagicMock
storage_client.storage_accounts = {}
with mock.patch(
"prowler.providers.azure.services.storage.storage_ensure_soft_delete_is_enabled.storage_ensure_soft_delete_is_enabled.storage_client",
new=storage_client,
):
from prowler.providers.azure.services.storage.storage_ensure_soft_delete_is_enabled.storage_ensure_soft_delete_is_enabled import (
storage_ensure_soft_delete_is_enabled,
)
check = storage_ensure_soft_delete_is_enabled()
result = check.execute()
assert len(result) == 0
def test_storage_no_blob_properties(self):
storage_account_id = str(uuid4())
storage_account_name = "Test Storage Account"
storage_client = mock.MagicMock
storage_account_blob_properties = None
storage_client.storage_accounts = {
AZURE_SUSCRIPTION: [
Storage_Account(
id=storage_account_id,
name=storage_account_name,
resouce_group_name=None,
enable_https_traffic_only=False,
infrastructure_encryption=False,
allow_blob_public_access=None,
network_rule_set=None,
encryption_type="None",
minimum_tls_version=None,
key_expiration_period_in_days=None,
private_endpoint_connections=None,
blob_properties=storage_account_blob_properties,
)
]
}
with mock.patch(
"prowler.providers.azure.services.storage.storage_ensure_soft_delete_is_enabled.storage_ensure_soft_delete_is_enabled.storage_client",
new=storage_client,
):
from prowler.providers.azure.services.storage.storage_ensure_soft_delete_is_enabled.storage_ensure_soft_delete_is_enabled import (
storage_ensure_soft_delete_is_enabled,
)
check = storage_ensure_soft_delete_is_enabled()
result = check.execute()
assert len(result) == 0
def test_storage_ensure_soft_delete_is_disabled(
self,
):
storage_account_id = str(uuid4())
storage_account_name = "Test Storage Account"
storage_client = mock.MagicMock
storage_account_blob_properties = Blob_Properties(
id=None,
name=None,
type=None,
default_service_version=None,
container_delete_retention_policy=DeleteRetentionPolicy(enabled=False),
)
storage_client.storage_accounts = {
AZURE_SUSCRIPTION: [
Storage_Account(
id=storage_account_id,
name=storage_account_name,
resouce_group_name=None,
enable_https_traffic_only=False,
infrastructure_encryption=False,
allow_blob_public_access=None,
network_rule_set=None,
encryption_type="None",
minimum_tls_version=None,
key_expiration_period_in_days=None,
private_endpoint_connections=None,
blob_properties=storage_account_blob_properties,
)
]
}
with mock.patch(
"prowler.providers.azure.services.storage.storage_ensure_soft_delete_is_enabled.storage_ensure_soft_delete_is_enabled.storage_client",
new=storage_client,
):
from prowler.providers.azure.services.storage.storage_ensure_soft_delete_is_enabled.storage_ensure_soft_delete_is_enabled import (
storage_ensure_soft_delete_is_enabled,
)
check = storage_ensure_soft_delete_is_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Storage account {storage_account_name} from subscription {AZURE_SUSCRIPTION} has soft delete disabled."
)
assert result[0].subscription == AZURE_SUSCRIPTION
assert result[0].resource_name == storage_account_name
assert result[0].resource_id == storage_account_id
def test_storage_ensure_soft_delete_is_enabled(
self,
):
storage_account_id = str(uuid4())
storage_account_name = "Test Storage Account"
storage_client = mock.MagicMock
storage_account_blob_properties = Blob_Properties(
id=None,
name=None,
type=None,
default_service_version=None,
container_delete_retention_policy=DeleteRetentionPolicy(enabled=True),
)
storage_client.storage_accounts = {
AZURE_SUSCRIPTION: [
Storage_Account(
id=storage_account_id,
name=storage_account_name,
resouce_group_name=None,
enable_https_traffic_only=False,
infrastructure_encryption=False,
allow_blob_public_access=None,
network_rule_set=None,
encryption_type="None",
minimum_tls_version=None,
key_expiration_period_in_days=None,
private_endpoint_connections=None,
blob_properties=storage_account_blob_properties,
)
]
}
with mock.patch(
"prowler.providers.azure.services.storage.storage_ensure_soft_delete_is_enabled.storage_ensure_soft_delete_is_enabled.storage_client",
new=storage_client,
):
from prowler.providers.azure.services.storage.storage_ensure_soft_delete_is_enabled.storage_ensure_soft_delete_is_enabled import (
storage_ensure_soft_delete_is_enabled,
)
check = storage_ensure_soft_delete_is_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"Storage account {storage_account_name} from subscription {AZURE_SUSCRIPTION} has soft delete enabled."
)
assert result[0].subscription == AZURE_SUSCRIPTION
assert result[0].resource_name == storage_account_name
assert result[0].resource_id == storage_account_id

View File

@@ -32,6 +32,7 @@ class Test_storage_infrastructure_encryption_is_enabled:
Storage_Account(
id=storage_account_id,
name=storage_account_name,
resouce_group_name=None,
enable_https_traffic_only=False,
infrastructure_encryption=False,
allow_blob_public_access=None,
@@ -73,6 +74,7 @@ class Test_storage_infrastructure_encryption_is_enabled:
Storage_Account(
id=storage_account_id,
name=storage_account_name,
resouce_group_name=None,
enable_https_traffic_only=False,
infrastructure_encryption=True,
allow_blob_public_access=None,

View File

@@ -32,6 +32,7 @@ class Test_storage_key_rotation_90_dayss:
Storage_Account(
id=storage_account_id,
name=storage_account_name,
resouce_group_name=None,
enable_https_traffic_only=False,
infrastructure_encryption=False,
allow_blob_public_access=None,
@@ -74,6 +75,7 @@ class Test_storage_key_rotation_90_dayss:
Storage_Account(
id=storage_account_id,
name=storage_account_name,
resouce_group_name=None,
enable_https_traffic_only=False,
infrastructure_encryption=False,
allow_blob_public_access=None,
@@ -115,6 +117,7 @@ class Test_storage_key_rotation_90_dayss:
Storage_Account(
id=storage_account_id,
name=storage_account_name,
resouce_group_name=None,
enable_https_traffic_only=False,
infrastructure_encryption=False,
allow_blob_public_access=None,

View File

@@ -32,6 +32,7 @@ class Test_storage_secure_transfer_required_is_enabled:
Storage_Account(
id=storage_account_id,
name=storage_account_name,
resouce_group_name=None,
enable_https_traffic_only=False,
infrastructure_encryption=False,
allow_blob_public_access=None,
@@ -73,6 +74,7 @@ class Test_storage_secure_transfer_required_is_enabled:
Storage_Account(
id=storage_account_id,
name=storage_account_name,
resouce_group_name=None,
enable_https_traffic_only=True,
infrastructure_encryption=True,
allow_blob_public_access=None,

View File

@@ -0,0 +1,126 @@
from unittest.mock import patch
from prowler.providers.azure.services.storage.storage_service import (
Blob_Properties,
Storage,
Storage_Account,
)
from tests.providers.azure.azure_fixtures import (
AZURE_SUSCRIPTION,
set_mocked_azure_audit_info,
)
def mock_storage_get_storage_accounts(_):
blob_properties = Blob_Properties(
id="id",
name="name",
type="type",
default_service_version=None,
container_delete_retention_policy=None,
)
return {
AZURE_SUSCRIPTION: [
Storage_Account(
id="id",
name="name",
resouce_group_name=None,
enable_https_traffic_only=False,
infrastructure_encryption=False,
allow_blob_public_access=None,
network_rule_set=None,
encryption_type="None",
minimum_tls_version=None,
key_expiration_period_in_days=None,
private_endpoint_connections=None,
blob_properties=blob_properties,
)
]
}
@patch(
"prowler.providers.azure.services.storage.storage_service.Storage.__get_storage_accounts__",
new=mock_storage_get_storage_accounts,
)
class Test_Storage_Service:
def test__get_client__(self):
storage = Storage(set_mocked_azure_audit_info())
assert (
storage.clients[AZURE_SUSCRIPTION].__class__.__name__
== "StorageManagementClient"
)
def test__get_storage_accounts__(self):
storage = Storage(set_mocked_azure_audit_info())
assert (
storage.storage_accounts[AZURE_SUSCRIPTION][0].__class__.__name__
== "Storage_Account"
)
assert storage.storage_accounts[AZURE_SUSCRIPTION][0].id == "id"
assert storage.storage_accounts[AZURE_SUSCRIPTION][0].name == "name"
assert storage.storage_accounts[AZURE_SUSCRIPTION][0].resouce_group_name is None
assert (
storage.storage_accounts[AZURE_SUSCRIPTION][0].enable_https_traffic_only
is False
)
assert (
storage.storage_accounts[AZURE_SUSCRIPTION][0].infrastructure_encryption
is False
)
assert (
storage.storage_accounts[AZURE_SUSCRIPTION][0].allow_blob_public_access
is None
)
assert storage.storage_accounts[AZURE_SUSCRIPTION][0].network_rule_set is None
assert storage.storage_accounts[AZURE_SUSCRIPTION][0].encryption_type == "None"
assert (
storage.storage_accounts[AZURE_SUSCRIPTION][0].minimum_tls_version is None
)
assert (
storage.storage_accounts[AZURE_SUSCRIPTION][0].key_expiration_period_in_days
is None
)
assert (
storage.storage_accounts[AZURE_SUSCRIPTION][0].private_endpoint_connections
is None
)
assert storage.storage_accounts[AZURE_SUSCRIPTION][
0
].blob_properties == Blob_Properties(
id="id",
name="name",
type="type",
default_service_version=None,
container_delete_retention_policy=None,
)
def test__get_blob_properties__(self):
storage = Storage(set_mocked_azure_audit_info())
assert (
storage.storage_accounts[AZURE_SUSCRIPTION][
0
].blob_properties.__class__.__name__
== "Blob_Properties"
)
assert storage.storage_accounts[AZURE_SUSCRIPTION][0].blob_properties.id == "id"
assert (
storage.storage_accounts[AZURE_SUSCRIPTION][0].blob_properties.name
== "name"
)
assert (
storage.storage_accounts[AZURE_SUSCRIPTION][0].blob_properties.type
== "type"
)
assert (
storage.storage_accounts[AZURE_SUSCRIPTION][
0
].blob_properties.default_service_version
is None
)
assert (
storage.storage_accounts[AZURE_SUSCRIPTION][
0
].blob_properties.container_delete_retention_policy
is None
)