feat(azure): AI Search service check not publicly accesible (#5846)

Co-authored-by: Rubén De la Torre Vico <rubendltv22@gmail.com>
Co-authored-by: MrCloudSec <hello@mistercloudsec.com>
This commit is contained in:
StylusFrost
2024-11-22 18:28:44 +01:00
committed by GitHub
parent 6811a22651
commit fee0bf3ea1
10 changed files with 294 additions and 4 deletions
Generated
+18 -4
View File
@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand.
[[package]]
name = "about-time"
@@ -561,6 +561,22 @@ azure-mgmt-core = ">=1.3.2"
isodate = ">=0.6.1"
typing-extensions = ">=4.6.0"
[[package]]
name = "azure-mgmt-search"
version = "9.1.0"
description = "Microsoft Azure Search Management Client Library for Python"
optional = false
python-versions = ">=3.7"
files = [
{file = "azure-mgmt-search-9.1.0.tar.gz", hash = "sha256:53bc6eeadb0974d21f120bb21bb5e6827df6d650e17347460fd83e2d68883599"},
{file = "azure_mgmt_search-9.1.0-py3-none-any.whl", hash = "sha256:488ff81477e980e2b7abf0b857387c74ebbad419e6f6126044e3e6fad2da72b6"},
]
[package.dependencies]
azure-common = ">=1.1,<2.0"
azure-mgmt-core = ">=1.3.2,<2.0.0"
isodate = ">=0.6.1,<1.0.0"
[[package]]
name = "azure-mgmt-security"
version = "7.0.0"
@@ -2078,8 +2094,6 @@ optional = false
python-versions = "*"
files = [
{file = "jsonpath-ng-1.7.0.tar.gz", hash = "sha256:f6f5f7fd4e5ff79c785f1573b394043b39849fb2bb47bcead935d12b00beab3c"},
{file = "jsonpath_ng-1.7.0-py2-none-any.whl", hash = "sha256:898c93fc173f0c336784a3fa63d7434297544b7198124a68f9a3ef9597b0ae6e"},
{file = "jsonpath_ng-1.7.0-py3-none-any.whl", hash = "sha256:f3d7f9e848cba1b6da28c55b1c26ff915dc9e0b1ba7e752a53d6da8d5cbd00b6"},
]
[package.dependencies]
@@ -5178,4 +5192,4 @@ type = ["pytest-mypy"]
[metadata]
lock-version = "2.0"
python-versions = ">=3.9,<3.13"
content-hash = "c7a86b55cbd211d342a9e00de00c7d13fcca90ba26649d80d144b70cae07606c"
content-hash = "0313d13861b3253896ca0b19d2f9317d576dfd37cc9003e89944306f1c2da666"
@@ -0,0 +1,4 @@
from prowler.providers.azure.services.aisearch.aisearch_service import AISearch
from prowler.providers.common.provider import Provider
aisearch_client = AISearch(Provider.get_global_provider())
@@ -0,0 +1,48 @@
from dataclasses import dataclass
from azure.mgmt.search import SearchManagementClient
from prowler.lib.logger import logger
from prowler.providers.azure.azure_provider import AzureProvider
from prowler.providers.azure.lib.service.service import AzureService
class AISearch(AzureService):
def __init__(self, provider: AzureProvider):
super().__init__(SearchManagementClient, provider)
self.aisearch_services = self._get_aisearch_services()
def _get_aisearch_services(self):
logger.info("AISearch - Getting services ...")
aisearch_services = {}
for subscription, client in self.clients.items():
try:
aisearch_services.update({subscription: {}})
aisearch_services_list = client.services.list_by_subscription()
for aisearch_service in aisearch_services_list:
aisearch_services[subscription].update(
{
aisearch_service.id: AISearchService(
name=aisearch_service.name,
location=aisearch_service.location,
public_network_access=(
False
if aisearch_service.public_network_access
== "Disabled"
else True
),
)
}
)
except Exception as error:
logger.error(
f"Subscription name: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
return aisearch_services
@dataclass
class AISearchService:
name: str
location: str
public_network_access: bool
@@ -0,0 +1,30 @@
{
"Provider": "azure",
"CheckID": "aisearch_service_not_publicly_accessible",
"CheckTitle": "Restrict public network access to the AI Search Service",
"CheckType": [],
"ServiceName": "aisearch",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "",
"Description": "Ensure that public network access to the Search Service is restricted.",
"Risk": "Public accessibility exposes the Search Service to potential attacks, unauthorized usage, and data breaches. Restricting access minimizes the surface area for attacks and ensures that only authorized networks can access the search service.",
"RelatedUrl": "https://learn.microsoft.com/en-us/azure/search/service-configure-firewall#configure-network-access-in-azure-portal",
"Remediation": {
"Code": {
"CLI": "az search service update --resource-group <resource_group_name> --name <service_name> --public-access disabled",
"NativeIaC": "",
"Other": "",
"Terraform": ""
},
"Recommendation": {
"Text": "Ensure that the necessary virtual network configurations or IP rules are in place to allow access from required services once public access is restricted. Review the network access settings regularly to maintain a secure environment. To restrict public network access to your Search Service: 1. Navigate to your Search Service y in the Azure Portal. 2. Under 'Settings'->'Networking', configure the 'Public network access' settings to 'Disabled'. 3. Set up virtual network service endpoints or private endpoints as needed for secure access. 4. Review and adjust IP access rules as necessary.",
"Url": "https://learn.microsoft.com/en-us/azure/search/service-configure-firewall#configure-network-access-in-azure-portal"
}
},
"Categories": [],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,30 @@
from typing import List
from prowler.lib.check.models import Check, Check_Report_Azure
from prowler.providers.azure.services.aisearch.aisearch_client import aisearch_client
class aisearch_service_not_publicly_accessible(Check):
def execute(self) -> List[Check_Report_Azure]:
findings = []
for (
subscription_name,
aisearch_services,
) in aisearch_client.aisearch_services.items():
for aisearch_service_id, aisearch_service in aisearch_services.items():
report = Check_Report_Azure(self.metadata())
report.subscription = subscription_name
report.resource_name = aisearch_service.name
report.resource_id = aisearch_service_id
report.location = aisearch_service.location
report.status = "FAIL"
report.status_extended = f"AISearch Service {aisearch_service.name} from subscription {subscription_name} allows public access."
if not aisearch_service.public_network_access:
report.status = "PASS"
report.status_extended = f"AISearch Service {aisearch_service.name} from subscription {subscription_name} does not allows public access."
findings.append(report)
return findings
+1
View File
@@ -41,6 +41,7 @@ azure-mgmt-monitor = "6.0.2"
azure-mgmt-network = "28.0.0"
azure-mgmt-rdbms = "10.1.0"
azure-mgmt-resource = "23.2.0"
azure-mgmt-search = "9.1.0"
azure-mgmt-security = "7.0.0"
azure-mgmt-sql = "3.0.1"
azure-mgmt-storage = "21.2.1"
@@ -0,0 +1,104 @@
from unittest import mock
from uuid import uuid4
from prowler.providers.azure.services.aisearch.aisearch_service import AISearchService
from tests.providers.azure.azure_fixtures import (
AZURE_SUBSCRIPTION_ID,
set_mocked_azure_provider,
)
class Test_AISearch_service_not_publicly_accessible:
def test_aisearch_sevice_no_aisearch_services(self):
aisearch_client = mock.MagicMock
aisearch_client.aisearch_services = {}
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
), mock.patch(
"prowler.providers.azure.services.aisearch.aisearch_service_not_publicly_accessible.aisearch_service_not_publicly_accessible.aisearch_client",
new=aisearch_client,
):
from prowler.providers.azure.services.aisearch.aisearch_service_not_publicly_accessible.aisearch_service_not_publicly_accessible import (
aisearch_service_not_publicly_accessible,
)
check = aisearch_service_not_publicly_accessible()
result = check.execute()
assert len(result) == 0
def test_aisearch_service_not_publicly_accessible_enabled(self):
aisearch_service_id = str(uuid4())
aisearch_service_name = "Test AISearch Service"
aisearch_client = mock.MagicMock
aisearch_client.aisearch_services = {
AZURE_SUBSCRIPTION_ID: {
aisearch_service_id: AISearchService(
name=aisearch_service_name,
location="westeurope",
public_network_access=True,
)
}
}
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
), mock.patch(
"prowler.providers.azure.services.aisearch.aisearch_service_not_publicly_accessible.aisearch_service_not_publicly_accessible.aisearch_client",
new=aisearch_client,
):
from prowler.providers.azure.services.aisearch.aisearch_service_not_publicly_accessible.aisearch_service_not_publicly_accessible import (
aisearch_service_not_publicly_accessible,
)
check = aisearch_service_not_publicly_accessible()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"AISearch Service {aisearch_service_name} from subscription {AZURE_SUBSCRIPTION_ID} allows public access."
)
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
assert result[0].resource_name == aisearch_service_name
assert result[0].location == "westeurope"
def test_aisearch_service_not_publicly_accessible_disabled(self):
aisearch_service_id = str(uuid4())
aisearch_service_name = "Test Search Service"
aisearch_client = mock.MagicMock
aisearch_client.aisearch_services = {
AZURE_SUBSCRIPTION_ID: {
aisearch_service_id: AISearchService(
name=aisearch_service_name,
location="westeurope",
public_network_access=False,
)
}
}
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
), mock.patch(
"prowler.providers.azure.services.aisearch.aisearch_service_not_publicly_accessible.aisearch_service_not_publicly_accessible.aisearch_client",
new=aisearch_client,
):
from prowler.providers.azure.services.aisearch.aisearch_service_not_publicly_accessible.aisearch_service_not_publicly_accessible import (
aisearch_service_not_publicly_accessible,
)
check = aisearch_service_not_publicly_accessible()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"AISearch Service {aisearch_service_name} from subscription {AZURE_SUBSCRIPTION_ID} does not allows public access."
)
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
assert result[0].resource_name == aisearch_service_name
assert result[0].resource_id == aisearch_service_id
assert result[0].location == "westeurope"
@@ -0,0 +1,59 @@
from unittest.mock import patch
from prowler.providers.azure.services.aisearch.aisearch_service import (
AISearch,
AISearchService,
)
from tests.providers.azure.azure_fixtures import (
AZURE_SUBSCRIPTION_ID,
set_mocked_azure_provider,
)
def mock_storage_get_aisearch_services(_):
return {
AZURE_SUBSCRIPTION_ID: {
"aisearch_service_id-1": AISearchService(
name="name",
location="westeurope",
public_network_access=True,
)
}
}
@patch(
"prowler.providers.azure.services.aisearch.aisearch_service.AISearch._get_aisearch_services",
new=mock_storage_get_aisearch_services,
)
class Test_AISearch_Service:
def test_get_client(self):
aisearch = AISearch(set_mocked_azure_provider())
assert (
aisearch.clients[AZURE_SUBSCRIPTION_ID].__class__.__name__
== "SearchManagementClient"
)
def test_get_aisearch_services(self):
aisearch = AISearch(set_mocked_azure_provider())
assert (
aisearch.aisearch_services[AZURE_SUBSCRIPTION_ID][
"aisearch_service_id-1"
].__class__.__name__
== "AISearchService"
)
assert (
aisearch.aisearch_services[AZURE_SUBSCRIPTION_ID][
"aisearch_service_id-1"
].name
== "name"
)
assert (
aisearch.aisearch_services[AZURE_SUBSCRIPTION_ID][
"aisearch_service_id-1"
].location
== "westeurope"
)
assert aisearch.aisearch_services[AZURE_SUBSCRIPTION_ID][
"aisearch_service_id-1"
].public_network_access