mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-23 20:42:02 +00:00
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:
Generated
+18
-4
@@ -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
|
||||
+30
@@ -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": ""
|
||||
}
|
||||
+30
@@ -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
|
||||
@@ -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"
|
||||
|
||||
+104
@@ -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
|
||||
Reference in New Issue
Block a user