feat(containerregistry): add new check containerregistry_uses_private_link (#5375)

Co-authored-by: Sergio Garcia <38561120+sergargar@users.noreply.github.com>
This commit is contained in:
johannes-engler-mw
2024-10-18 22:15:52 +02:00
committed by GitHub
parent 0c82137834
commit 2b34fd39f6
16 changed files with 252 additions and 24 deletions
@@ -14,11 +14,11 @@ class appinsights_ensure_is_configured(Check):
report.subscription = subscription_name
report.resource_name = "AppInsights"
report.resource_id = "AppInsights"
report.status_extended = f"There is at least one AppInsight configured in susbscription {subscription_name}."
report.status_extended = f"There is at least one AppInsight configured in subscription {subscription_name}."
if len(components) < 1:
report.status = "FAIL"
report.status_extended = f"There are no AppInsight configured in susbscription {subscription_name}."
report.status_extended = f"There are no AppInsight configured in subscription {subscription_name}."
findings.append(report)
@@ -1,7 +1,10 @@
from dataclasses import dataclass
from azure.mgmt.containerregistry import ContainerRegistryManagementClient
from azure.mgmt.containerregistry.models import NetworkRuleSet
from azure.mgmt.containerregistry.models import (
NetworkRuleSet,
PrivateEndpointConnection,
)
from prowler.lib.logger import logger
from prowler.providers.azure.azure_provider import AzureProvider
@@ -46,6 +49,9 @@ class ContainerRegistry(AzureService):
monitor_diagnostic_settings=self._get_registry_monitor_settings(
registry.name, resource_group, subscription
),
private_endpoint_connections=getattr(
registry, "private_endpoint_connections", []
),
)
},
)
@@ -91,3 +97,4 @@ class ContainerRegistryInfo:
admin_user_enabled: bool
network_rule_set: NetworkRuleSet
monitor_diagnostic_settings: list[DiagnosticSetting]
private_endpoint_connections: list[PrivateEndpointConnection]
@@ -0,0 +1,30 @@
{
"Provider": "azure",
"CheckID": "containerregistry_uses_private_link",
"CheckTitle": "Ensure to use a private link for accessing the Azure Container Registry",
"CheckType": [],
"ServiceName": "containerregistry",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "ContainerRegistry",
"Description": "Ensure that a private link is used for accessing the Azure Container Registry to enhance security and restrict access to the registry over the public internet.",
"Risk": "Without using a private link, the Azure Container Registry may be exposed to the public internet, increasing the risk of unauthorized access and potential data breaches.",
"RelatedUrl": "https://learn.microsoft.com/en-us/azure/private-link/private-link-overview",
"Remediation": {
"Code": {
"CLI": "az network private-endpoint create --connection-name <ConnectionName> --resource-group <ResourceGroupName> --name <Name> --private-connection-resource-id <RegistryId> --vnet-name <VnetName> --subnet <SubnetName> --group-ids registry",
"NativeIaC": "",
"Other": "",
"Terraform": ""
},
"Recommendation": {
"Text": "Create a private link for Azure Container Registry through the Azure Portal: 1. Navigate to your Container Registry. 2. In the settings, select 'Networking'. 3. Select 'Private access'. 4. Configure a private endpoint for the registry.",
"Url": "https://learn.microsoft.com/en-us/azure/container-registry/container-registry-private-link"
}
},
"Categories": [],
"DependsOn": [],
"RelatedTo": [],
"Notes": "This feature is only available for Premium SKU registries."
}
@@ -0,0 +1,27 @@
from prowler.lib.check.models import Check, Check_Report_Azure
from prowler.providers.azure.services.containerregistry.containerregistry_client import (
containerregistry_client,
)
class containerregistry_uses_private_link(Check):
def execute(self) -> list[Check_Report_Azure]:
findings = []
for subscription, registries in containerregistry_client.registries.items():
for registry_id, container_registry_info in registries.items():
report = Check_Report_Azure(self.metadata())
report.subscription = subscription
report.resource_name = container_registry_info.name
report.resource_id = registry_id
report.location = container_registry_info.location
report.status = "FAIL"
report.status_extended = f"Container Registry {container_registry_info.name} from subscription {subscription} does not use a private link."
if container_registry_info.private_endpoint_connections:
report.status = "PASS"
report.status_extended = f"Container Registry {container_registry_info.name} from subscription {subscription} uses a private link."
findings.append(report)
return findings
@@ -18,7 +18,7 @@ class defender_additional_email_configured_with_a_security_contact(Check):
report.subscription = subscription_name
report.resource_name = contact_name
report.resource_id = contact_info.resource_id
report.status_extended = f"There is another correct email configured for susbscription {subscription_name}."
report.status_extended = f"There is another correct email configured for subscription {subscription_name}."
emails = contact_info.emails.split(";")
@@ -29,7 +29,7 @@ class defender_additional_email_configured_with_a_security_contact(Check):
break
else:
report.status = "FAIL"
report.status_extended = f"There is not another correct email configured for susbscription {subscription_name}."
report.status_extended = f"There is not another correct email configured for subscription {subscription_name}."
findings.append(report)
@@ -30,11 +30,11 @@ class defender_ensure_iot_hub_defender_is_on(Check):
report.subscription = subscription_name
report.resource_name = iot_security_solution_name
report.resource_id = iot_security_solution.resource_id
report.status_extended = f"The security solution {iot_security_solution_name} is enabled in susbscription {subscription_name}."
report.status_extended = f"The security solution {iot_security_solution_name} is enabled in subscription {subscription_name}."
if iot_security_solution.status != "Enabled":
report.status = "FAIL"
report.status_extended = f"The security solution {iot_security_solution_name} is disabled in susbscription {subscription_name}"
report.status_extended = f"The security solution {iot_security_solution_name} is disabled in subscription {subscription_name}"
findings.append(report)
@@ -16,11 +16,11 @@ class defender_ensure_notify_alerts_severity_is_high(Check):
report.subscription = subscription_name
report.resource_name = contact_name
report.resource_id = contact_info.resource_id
report.status_extended = f"Notifiy alerts are enabled for severity high in susbscription {subscription_name}."
report.status_extended = f"Notifiy alerts are enabled for severity high in subscription {subscription_name}."
if contact_info.alert_notifications_minimal_severity != "High":
report.status = "FAIL"
report.status_extended = f"Notifiy alerts are not enabled for severity high in susbscription {subscription_name}."
report.status_extended = f"Notifiy alerts are not enabled for severity high in subscription {subscription_name}."
findings.append(report)
@@ -52,7 +52,7 @@ class Test_appinsights_ensure_is_configured:
assert result[0].location == "global"
assert (
result[0].status_extended
== f"There are no AppInsight configured in susbscription {AZURE_SUBSCRIPTION_ID}."
== f"There are no AppInsight configured in subscription {AZURE_SUBSCRIPTION_ID}."
)
def test_appinsights_configured(self):
@@ -89,5 +89,5 @@ class Test_appinsights_ensure_is_configured:
assert result[0].location == "global"
assert (
result[0].status_extended
== f"There is at least one AppInsight configured in susbscription {AZURE_SUBSCRIPTION_ID}."
== f"There is at least one AppInsight configured in subscription {AZURE_SUBSCRIPTION_ID}."
)
@@ -59,6 +59,7 @@ class TestContainerRegistryAdminUserDisabled:
admin_user_enabled=True,
network_rule_set=None,
monitor_diagnostic_settings=[],
private_endpoint_connections=[],
)
}
}
@@ -115,6 +116,7 @@ class TestContainerRegistryAdminUserDisabled:
admin_user_enabled=False,
network_rule_set=None,
monitor_diagnostic_settings=[],
private_endpoint_connections=[],
)
}
}
@@ -60,6 +60,7 @@ class Test_containerregistry_not_publicly_accessible:
public_network_access="Enabled",
admin_user_enabled=True,
network_rule_set=NetworkRuleSet(default_action="Allow"),
private_endpoint_connections=[],
monitor_diagnostic_settings=[
{
"id": "id1/id1",
@@ -133,6 +134,7 @@ class Test_containerregistry_not_publicly_accessible:
public_network_access="Enabled",
admin_user_enabled=False,
network_rule_set=NetworkRuleSet(default_action="Deny"),
private_endpoint_connections=[],
monitor_diagnostic_settings=[
{
"id": "id1/id1",
@@ -35,6 +35,7 @@ class TestContainerRegistryService:
public_network_access="Enabled",
admin_user_enabled=True,
network_rule_set=None,
private_endpoint_connections=[],
monitor_diagnostic_settings=[
{
"id": "id1/id1",
@@ -0,0 +1,159 @@
from unittest import mock
from unittest.mock import MagicMock
from uuid import uuid4
from azure.mgmt.containerregistry.models import (
PrivateEndpoint,
PrivateEndpointConnection,
PrivateLinkServiceConnectionState,
)
from tests.providers.azure.azure_fixtures import (
AZURE_SUBSCRIPTION_ID,
set_mocked_azure_provider,
)
class Test_containerregistry_uses_private_link:
def test_no_container_registries(self):
containerregistry_client = MagicMock()
containerregistry_client.registries = {}
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
), mock.patch(
"prowler.providers.azure.services.containerregistry.containerregistry_uses_private_link.containerregistry_uses_private_link.containerregistry_client",
new=containerregistry_client,
):
from prowler.providers.azure.services.containerregistry.containerregistry_uses_private_link.containerregistry_uses_private_link import (
containerregistry_uses_private_link,
)
check = containerregistry_uses_private_link()
result = check.execute()
assert len(result) == 0
def test_container_registry_not_uses_private_link(self):
containerregistry_client = MagicMock()
registry_id = str(uuid4())
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
), mock.patch(
"prowler.providers.azure.services.containerregistry.containerregistry_uses_private_link.containerregistry_uses_private_link.containerregistry_client",
new=containerregistry_client,
):
from prowler.providers.azure.services.containerregistry.containerregistry_service import (
ContainerRegistryInfo,
)
from prowler.providers.azure.services.containerregistry.containerregistry_uses_private_link.containerregistry_uses_private_link import (
containerregistry_uses_private_link,
)
containerregistry_client.registries = {
AZURE_SUBSCRIPTION_ID: {
registry_id: ContainerRegistryInfo(
id=registry_id,
name="mock_registry",
location="westeurope",
resource_group="mock_resource_group",
sku="Basic",
login_server="mock_login_server.azurecr.io",
public_network_access="Enabled",
admin_user_enabled=True,
monitor_diagnostic_settings=[],
network_rule_set=[],
private_endpoint_connections=[],
)
}
}
check = containerregistry_uses_private_link()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Container Registry mock_registry from subscription {AZURE_SUBSCRIPTION_ID} does not use a private link."
)
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
assert result[0].resource_name == "mock_registry"
assert (
result[0].resource_id
== containerregistry_client.registries[AZURE_SUBSCRIPTION_ID][
registry_id
].id
)
assert result[0].location == "westeurope"
def test_container_registry_uses_private_link(self):
containerregistry_client = mock.MagicMock()
containerregistry_client.registries = {}
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
), mock.patch(
"prowler.providers.azure.services.containerregistry.containerregistry_uses_private_link.containerregistry_uses_private_link.containerregistry_client",
new=containerregistry_client,
):
from prowler.providers.azure.services.containerregistry.containerregistry_service import (
ContainerRegistryInfo,
)
from prowler.providers.azure.services.containerregistry.containerregistry_uses_private_link.containerregistry_uses_private_link import (
containerregistry_uses_private_link,
)
registry_id = str(uuid4())
containerregistry_client.registries = {
AZURE_SUBSCRIPTION_ID: {
registry_id: ContainerRegistryInfo(
id=registry_id,
name="mock_registry",
location="westeurope",
resource_group="mock_resource_group",
sku="Basic",
login_server="mock_login_server.azurecr.io",
public_network_access="Enabled",
admin_user_enabled=False,
monitor_diagnostic_settings=[],
network_rule_set=[],
private_endpoint_connections=[
PrivateEndpointConnection(
id="/subscriptions/AZURE_SUBSCRIPTION_ID/resourceGroups/mock_resource_group/providers/Microsoft.ContainerRegistry/registries/mock_registry/privateEndpointConnections/myConnection",
private_endpoint=PrivateEndpoint(
id="/subscriptions/AZURE_SUBSCRIPTION_ID/resourceGroups/mock_resource_group/providers/Microsoft.Network/privateEndpoints/myPrivateEndpoint"
),
private_link_service_connection_state=PrivateLinkServiceConnectionState(
status="Approved",
description="Auto-approved connection",
actions_required="None",
),
provisioning_state="Succeeded",
)
],
)
}
}
check = containerregistry_uses_private_link()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"Container Registry mock_registry from subscription {AZURE_SUBSCRIPTION_ID} uses a private link."
)
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
assert result[0].resource_name == "mock_registry"
assert (
result[0].resource_id
== containerregistry_client.registries[AZURE_SUBSCRIPTION_ID][
registry_id
].id
)
assert result[0].location == "westeurope"
@@ -62,7 +62,7 @@ class Test_defender_additional_email_configured_with_a_security_contact:
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"There is not another correct email configured for susbscription {AZURE_SUBSCRIPTION_ID}."
== f"There is not another correct email configured for subscription {AZURE_SUBSCRIPTION_ID}."
)
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
assert result[0].resource_name == "default"
@@ -102,7 +102,7 @@ class Test_defender_additional_email_configured_with_a_security_contact:
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"There is not another correct email configured for susbscription {AZURE_SUBSCRIPTION_ID}."
== f"There is not another correct email configured for subscription {AZURE_SUBSCRIPTION_ID}."
)
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
assert result[0].resource_name == "default"
@@ -142,7 +142,7 @@ class Test_defender_additional_email_configured_with_a_security_contact:
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"There is not another correct email configured for susbscription {AZURE_SUBSCRIPTION_ID}."
== f"There is not another correct email configured for subscription {AZURE_SUBSCRIPTION_ID}."
)
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
assert result[0].resource_name == "default"
@@ -182,7 +182,7 @@ class Test_defender_additional_email_configured_with_a_security_contact:
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"There is another correct email configured for susbscription {AZURE_SUBSCRIPTION_ID}."
== f"There is another correct email configured for subscription {AZURE_SUBSCRIPTION_ID}."
)
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
assert result[0].resource_name == "default"
@@ -222,7 +222,7 @@ class Test_defender_additional_email_configured_with_a_security_contact:
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"There is another correct email configured for susbscription {AZURE_SUBSCRIPTION_ID}."
== f"There is another correct email configured for subscription {AZURE_SUBSCRIPTION_ID}."
)
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
assert result[0].resource_name == "default"
@@ -261,7 +261,7 @@ class Test_defender_additional_email_configured_with_a_security_contact:
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"There is not another correct email configured for susbscription {AZURE_SUBSCRIPTION_ID}."
== f"There is not another correct email configured for subscription {AZURE_SUBSCRIPTION_ID}."
)
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
assert result[0].resource_name == "default"
@@ -84,7 +84,7 @@ class Test_defender_ensure_iot_hub_defender_is_on:
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"The security solution iot_sec_solution is disabled in susbscription {AZURE_SUBSCRIPTION_ID}"
== f"The security solution iot_sec_solution is disabled in subscription {AZURE_SUBSCRIPTION_ID}"
)
assert result[0].resource_name == "iot_sec_solution"
assert result[0].resource_id == resource_id
@@ -117,7 +117,7 @@ class Test_defender_ensure_iot_hub_defender_is_on:
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"The security solution iot_sec_solution is enabled in susbscription {AZURE_SUBSCRIPTION_ID}."
== f"The security solution iot_sec_solution is enabled in subscription {AZURE_SUBSCRIPTION_ID}."
)
assert result[0].resource_name == "iot_sec_solution"
assert result[0].resource_id == resource_id
@@ -155,7 +155,7 @@ class Test_defender_ensure_iot_hub_defender_is_on:
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"The security solution iot_sec_solution_enabled is enabled in susbscription {AZURE_SUBSCRIPTION_ID}."
== f"The security solution iot_sec_solution_enabled is enabled in subscription {AZURE_SUBSCRIPTION_ID}."
)
assert result[0].resource_name == "iot_sec_solution_enabled"
assert result[0].resource_id == resource_id_enabled
@@ -164,7 +164,7 @@ class Test_defender_ensure_iot_hub_defender_is_on:
assert result[1].status == "FAIL"
assert (
result[1].status_extended
== f"The security solution iot_sec_solution_disabled is disabled in susbscription {AZURE_SUBSCRIPTION_ID}"
== f"The security solution iot_sec_solution_disabled is disabled in subscription {AZURE_SUBSCRIPTION_ID}"
)
assert result[1].resource_name == "iot_sec_solution_disabled"
assert result[1].resource_id == resource_id_disabled
@@ -62,7 +62,7 @@ class Test_defender_ensure_notify_alerts_severity_is_high:
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Notifiy alerts are not enabled for severity high in susbscription {AZURE_SUBSCRIPTION_ID}."
== f"Notifiy alerts are not enabled for severity high in subscription {AZURE_SUBSCRIPTION_ID}."
)
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
assert result[0].resource_name == "default"
@@ -102,7 +102,7 @@ class Test_defender_ensure_notify_alerts_severity_is_high:
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"Notifiy alerts are enabled for severity high in susbscription {AZURE_SUBSCRIPTION_ID}."
== f"Notifiy alerts are enabled for severity high in subscription {AZURE_SUBSCRIPTION_ID}."
)
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
assert result[0].resource_name == "default"
@@ -141,7 +141,7 @@ class Test_defender_ensure_notify_alerts_severity_is_high:
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Notifiy alerts are not enabled for severity high in susbscription {AZURE_SUBSCRIPTION_ID}."
== f"Notifiy alerts are not enabled for severity high in subscription {AZURE_SUBSCRIPTION_ID}."
)
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
assert result[0].resource_name == "default"