feat(azure): add aiservices service with network, local auth and CMK checks (#12925)

Co-authored-by: pedrooot <pedromarting3@gmail.com>
This commit is contained in:
Rajesh Nadimintiandpedrooot authored and GitHub committed 2026-10-14 09:03:25 +02:00
1 parent 22468c1443
commit 8b814b7643
19 files changed
+831

No files matched your search

@@ -0,0 +1 @@
Azure AI Services service for Azure provider, including the `aiservices_account_public_network_access_disabled`, `aiservices_account_local_auth_disabled`, and `aiservices_account_encrypted_with_cmk` checks
Whitespace-only changes.
@@ -0,0 +1,38 @@
{
"Provider": "azure",
"CheckID": "aiservices_account_encrypted_with_cmk",
"CheckTitle": "AI services account is encrypted with a customer-managed key",
"CheckType": [],
"ServiceName": "aiservices",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "microsoft.cognitiveservices/accounts",
"ResourceGroup": "ai_ml",
"Description": "**Azure AI services accounts** (including **Azure OpenAI** and **Azure AI Foundry**) encrypt stored data such as fine-tuning files, custom models, and batch inputs. Setting the encryption **key source** to `Microsoft.KeyVault` uses a **customer-managed key** held in Azure Key Vault instead of a Microsoft-managed key.",
"Risk": "With Microsoft-managed keys the organization cannot:\n- **Revoke** access to stored AI data by disabling its own key\n- **Rotate** keys on its own schedule or audit key usage in Key Vault\n- **Meet** regulatory requirements that mandate customer-controlled encryption keys",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/azure/ai-services/encryption/cognitive-services-encryption-keys-portal",
"https://learn.microsoft.com/en-us/azure/foundry-classic/openai/encrypt-data-at-rest"
],
"Remediation": {
"Code": {
"CLI": "az cognitiveservices account update --name <example_resource_name> --resource-group <resource_group> --encryption '{\"keySource\":\"Microsoft.KeyVault\",\"keyVaultProperties\":{\"keyVaultUri\":\"<key_vault_uri>\",\"keyName\":\"<key_name>\",\"keyVersion\":\"<key_version>\"}}'",
"NativeIaC": "```bicep\n// Encrypt an Azure AI services account with a customer-managed key\nresource account 'Microsoft.CognitiveServices/accounts@2024-10-01' = {\n name: '<example_resource_name>'\n location: '<location>'\n kind: 'OpenAI'\n sku: { name: 'S0' }\n identity: { type: 'SystemAssigned' }\n properties: {\n customSubDomainName: '<example_resource_name>'\n encryption: {\n keySource: 'Microsoft.KeyVault' // CRITICAL: use a customer-managed key\n keyVaultProperties: {\n keyName: '<key_name>'\n keyVaultUri: '<key_vault_uri>'\n }\n }\n }\n}\n```",
"Other": "1. Create a key in an Azure Key Vault with soft delete and purge protection enabled\n2. Enable a managed identity on the AI services account and grant it get, wrapKey, and unwrapKey on the key\n3. In the Azure portal, open the AI services account and go to Resource Management > Encryption\n4. Select Customer Managed Keys, choose the key, and click Save",
"Terraform": "```hcl\nresource \"azurerm_cognitive_account_customer_managed_key\" \"<example_resource_name>\" {\n cognitive_account_id = azurerm_cognitive_account.<example_resource_name>.id\n key_vault_key_id = azurerm_key_vault_key.<example_resource_name>.id # CRITICAL: use a customer-managed key\n}\n```"
},
"Recommendation": {
"Text": "Encrypt AI services accounts that store sensitive data with a **customer-managed key** in Azure Key Vault. Enable **soft delete** and **purge protection** on the vault, grant the account's **managed identity** only the key permissions it needs, and rotate the key on a defined schedule.",
"Url": "https://hub.prowler.com/check/aiservices_account_encrypted_with_cmk"
}
},
"Categories": [
"encryption",
"gen-ai"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,53 @@
from prowler.lib.check.models import Check, Check_Report_Azure
from prowler.providers.azure.services.aiservices.aiservices_client import (
aiservices_client,
)
from prowler.providers.azure.services.aiservices.aiservices_service import Account
KEY_VAULT_KEY_SOURCE = "Microsoft.KeyVault"
def uses_customer_managed_key(account: Account) -> bool:
"""Decide whether the account is encrypted with a customer-managed key.
Args:
account: AI services account.
Returns:
True when the account counts as encrypted with a customer-managed key.
A Key Vault key source passes even when no key name is reported.
"""
return (account.encryption_key_source or "").lower() == KEY_VAULT_KEY_SOURCE.lower()
class aiservices_account_encrypted_with_cmk(Check):
"""AI services account is encrypted with a customer-managed key."""
def execute(self) -> list[Check_Report_Azure]:
"""Evaluate encryption key source on every AI services account.
Returns:
One finding per account.
"""
findings = []
for subscription_id, accounts in aiservices_client.accounts.items():
subscription_name = aiservices_client.subscriptions.get(
subscription_id, subscription_id
)
for account in accounts.values():
report = Check_Report_Azure(metadata=self.metadata(), resource=account)
report.subscription = subscription_id
prefix = f"AI services account {account.name} (kind {account.kind}) from subscription {subscription_name} ({subscription_id})"
if uses_customer_managed_key(account):
report.status = "PASS"
if account.encryption_key_name:
report.status_extended = f"{prefix} is encrypted with customer-managed key {account.encryption_key_name}."
else:
report.status_extended = f"{prefix} is encrypted with a customer-managed key from Azure Key Vault."
else:
report.status = "FAIL"
report.status_extended = (
f"{prefix} is encrypted with a Microsoft-managed key."
)
findings.append(report)
return findings
@@ -0,0 +1,38 @@
{
"Provider": "azure",
"CheckID": "aiservices_account_local_auth_disabled",
"CheckTitle": "AI services account has local (API key) authentication disabled",
"CheckType": [],
"ServiceName": "aiservices",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "microsoft.cognitiveservices/accounts",
"ResourceGroup": "ai_ml",
"Description": "**Azure AI services accounts** (including **Azure OpenAI** and **Azure AI Foundry**) accept static **API keys** unless `disableLocalAuth` is `true`. With local authentication disabled, every request must use a **Microsoft Entra ID** token.",
"Risk": "Static API keys never expire and carry no caller identity:\n- **Confidentiality**: a leaked key grants full data-plane access to models and data\n- **Accountability**: requests cannot be traced to a user or workload\n- **Control**: Conditional Access, RBAC scoping, and token lifetimes do not apply to keys",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/azure/ai-services/disable-local-auth",
"https://learn.microsoft.com/en-us/azure/ai-services/authentication"
],
"Remediation": {
"Code": {
"CLI": "az resource update --ids <account_resource_id> --set properties.disableLocalAuth=true",
"NativeIaC": "```bicep\n// Require Microsoft Entra ID for an Azure AI services account\nresource account 'Microsoft.CognitiveServices/accounts@2024-10-01' = {\n name: '<example_resource_name>'\n location: '<location>'\n kind: 'OpenAI'\n sku: { name: 'S0' }\n properties: {\n customSubDomainName: '<example_resource_name>'\n disableLocalAuth: true // CRITICAL: rejects API keys, requires Entra ID tokens\n }\n}\n```",
"Other": "1. Move every client to Microsoft Entra ID authentication (managed identity or service principal with the Cognitive Services User role)\n2. Run the Azure CLI command above, or deploy the Bicep or Terraform example\n3. Confirm that requests sent with an API key now return 401",
"Terraform": "```hcl\nresource \"azurerm_cognitive_account\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n location = \"<location>\"\n resource_group_name = \"<example_resource_name>\"\n kind = \"OpenAI\"\n sku_name = \"S0\"\n custom_subdomain_name = \"<example_resource_name>\"\n\n local_auth_enabled = false # CRITICAL: rejects API keys, requires Entra ID tokens\n}\n```"
},
"Recommendation": {
"Text": "Set `disableLocalAuth` to `true` and authenticate clients with **managed identities** or service principals through **Microsoft Entra ID**. Grant least-privilege roles such as **Cognitive Services OpenAI User** instead of sharing keys.",
"Url": "https://hub.prowler.com/check/aiservices_account_local_auth_disabled"
}
},
"Categories": [
"identity-access",
"gen-ai"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,34 @@
from prowler.lib.check.models import Check, Check_Report_Azure
from prowler.providers.azure.services.aiservices.aiservices_client import (
aiservices_client,
)
class aiservices_account_local_auth_disabled(Check):
"""AI services account has local (API key) authentication disabled."""
def execute(self) -> list[Check_Report_Azure]:
"""Evaluate local authentication on every AI services account.
Returns:
One finding per account.
"""
findings = []
for subscription_id, accounts in aiservices_client.accounts.items():
subscription_name = aiservices_client.subscriptions.get(
subscription_id, subscription_id
)
for account in accounts.values():
report = Check_Report_Azure(metadata=self.metadata(), resource=account)
report.subscription = subscription_id
prefix = f"AI services account {account.name} (kind {account.kind}) from subscription {subscription_name} ({subscription_id})"
if account.disable_local_auth:
report.status = "PASS"
report.status_extended = f"{prefix} has local (API key) authentication disabled and requires Microsoft Entra ID."
else:
report.status = "FAIL"
report.status_extended = (
f"{prefix} allows local (API key) authentication."
)
findings.append(report)
return findings
@@ -0,0 +1,38 @@
{
"Provider": "azure",
"CheckID": "aiservices_account_public_network_access_disabled",
"CheckTitle": "AI services account has public network access disabled",
"CheckType": [],
"ServiceName": "aiservices",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "microsoft.cognitiveservices/accounts",
"ResourceGroup": "ai_ml",
"Description": "**Azure AI services accounts** (including **Azure OpenAI** and **Azure AI Foundry**) can block their data-plane endpoint from the public internet by setting **public network access** to `Disabled`, so clients connect only through **private endpoints**.",
"Risk": "An internet-reachable AI endpoint exposes:\n- **Confidentiality**: prompts, completions, and grounding data to anyone holding a leaked key or token\n- **Integrity**: unauthorized model calls and deployment changes\n- **Availability**: quota exhaustion and cost abuse from scanning and automated traffic",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/azure/ai-services/cognitive-services-virtual-networks",
"https://learn.microsoft.com/en-us/security/benchmark/azure/baselines/cognitive-services-security-baseline"
],
"Remediation": {
"Code": {
"CLI": "az resource update --ids <account_resource_id> --set properties.publicNetworkAccess=Disabled",
"NativeIaC": "```bicep\n// Disable public network access for an Azure AI services account\nresource account 'Microsoft.CognitiveServices/accounts@2024-10-01' = {\n name: '<example_resource_name>'\n location: '<location>'\n kind: 'OpenAI'\n sku: { name: 'S0' }\n properties: {\n customSubDomainName: '<example_resource_name>'\n publicNetworkAccess: 'Disabled' // CRITICAL: blocks access from the public internet\n }\n}\n```",
"Other": "1. In the Azure portal, open the Azure AI services or Azure OpenAI resource\n2. Go to Resource Management > Networking\n3. Under Firewalls and virtual networks, select Disabled\n4. Click Save",
"Terraform": "```hcl\nresource \"azurerm_cognitive_account\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n location = \"<location>\"\n resource_group_name = \"<example_resource_name>\"\n kind = \"OpenAI\"\n sku_name = \"S0\"\n custom_subdomain_name = \"<example_resource_name>\"\n\n public_network_access_enabled = false # CRITICAL: blocks access from the public internet\n}\n```"
},
"Recommendation": {
"Text": "Set **public network access** to `Disabled` and reach the account through **Private Link**. If public access is temporarily required, restrict it to specific IP ranges or virtual networks and require Microsoft Entra ID authentication.",
"Url": "https://hub.prowler.com/check/aiservices_account_public_network_access_disabled"
}
},
"Categories": [
"internet-exposed",
"gen-ai"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,36 @@
from prowler.lib.check.models import Check, Check_Report_Azure
from prowler.providers.azure.services.aiservices.aiservices_client import (
aiservices_client,
)
class aiservices_account_public_network_access_disabled(Check):
"""AI services account has public network access disabled."""
def execute(self) -> list[Check_Report_Azure]:
"""Evaluate public network access on every AI services account.
Returns:
One finding per account.
"""
findings = []
for subscription_id, accounts in aiservices_client.accounts.items():
subscription_name = aiservices_client.subscriptions.get(
subscription_id, subscription_id
)
for account in accounts.values():
report = Check_Report_Azure(metadata=self.metadata(), resource=account)
report.subscription = subscription_id
prefix = f"AI services account {account.name} (kind {account.kind}) from subscription {subscription_name} ({subscription_id})"
if account.public_network_access:
report.status = "FAIL"
report.status_extended = (
f"{prefix} has public network access enabled."
)
else:
report.status = "PASS"
report.status_extended = (
f"{prefix} has public network access disabled."
)
findings.append(report)
return findings
@@ -0,0 +1,4 @@
from prowler.providers.azure.services.aiservices.aiservices_service import AIServices
from prowler.providers.common.provider import Provider
aiservices_client = AIServices(Provider.get_global_provider())
@@ -0,0 +1,97 @@
from typing import Optional
from azure.mgmt.cognitiveservices import CognitiveServicesManagementClient
from pydantic.v1 import BaseModel
from prowler.lib.logger import logger
from prowler.providers.azure.azure_provider import AzureProvider
from prowler.providers.azure.lib.service.service import AzureService
MICROSOFT_MANAGED_KEY_SOURCE = "Microsoft.CognitiveServices"
class AIServices(AzureService):
"""Azure AI services (Microsoft.CognitiveServices/accounts).
Covers every account kind, including Azure OpenAI (`OpenAI`) and
Azure AI Foundry (`AIServices`).
"""
def __init__(self, provider: AzureProvider):
super().__init__(CognitiveServicesManagementClient, provider)
self.accounts = self._get_accounts()
def _get_accounts(self) -> dict[str, dict[str, "Account"]]:
"""Get Azure AI services accounts for every audited subscription.
Returns:
Accounts keyed by subscription ID, then by account resource ID.
"""
logger.info("AIServices - Getting accounts...")
accounts = {}
for subscription, client in self.clients.items():
accounts[subscription] = {}
try:
sdk_accounts = self.list_with_rg_scope(
subscription,
client.accounts.list,
client.accounts.list_by_resource_group,
)
except Exception as error:
logger.error(
f"Subscription ID: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
continue
for sdk_account in sdk_accounts:
try:
account = self._to_account(sdk_account)
accounts[subscription][account.id] = account
except Exception as error:
logger.error(
f"Subscription ID: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
return accounts
@staticmethod
def _to_account(sdk_account) -> "Account":
"""Map an SDK account to the Prowler model.
Unset properties follow Azure defaults: public network access on,
local (key) authentication on, and Microsoft-managed encryption.
Args:
sdk_account: `azure.mgmt.cognitiveservices.models.Account`.
Returns:
The mapped `Account`.
"""
properties = sdk_account.properties
encryption = getattr(properties, "encryption", None)
key_vault_properties = getattr(encryption, "key_vault_properties", None)
return Account(
id=sdk_account.id,
name=sdk_account.name,
location=sdk_account.location,
kind=sdk_account.kind or "",
public_network_access=(
getattr(properties, "public_network_access", None) != "Disabled"
),
disable_local_auth=bool(getattr(properties, "disable_local_auth", False)),
encryption_key_source=(
getattr(encryption, "key_source", None) or MICROSOFT_MANAGED_KEY_SOURCE
),
encryption_key_name=getattr(key_vault_properties, "key_name", None),
)
class Account(BaseModel):
"""Azure AI services account."""
id: str
name: str
location: str
kind: str
public_network_access: bool
disable_local_auth: bool
encryption_key_source: str
encryption_key_name: Optional[str] = None
+1
View File
@@ -41,6 +41,7 @@ dependencies = [
"azure-keyvault-keys==4.10.0",
"azure-mgmt-applicationinsights==4.1.0",
"azure-mgmt-authorization==4.0.0",
"azure-mgmt-cognitiveservices==14.1.0",
"azure-mgmt-compute==34.0.0",
"azure-mgmt-containerregistry==12.0.0",
"azure-mgmt-containerservice==34.1.0",
@@ -0,0 +1,123 @@
from unittest import mock
from prowler.providers.azure.services.aiservices.aiservices_service import Account
from tests.providers.azure.azure_fixtures import (
AZURE_SUBSCRIPTION_DISPLAY,
AZURE_SUBSCRIPTION_ID,
AZURE_SUBSCRIPTION_NAME,
RESOURCE_GROUP,
set_mocked_azure_provider,
)
CHECK = "aiservices_account_encrypted_with_cmk"
CHECK_PATH = f"prowler.providers.azure.services.aiservices.{CHECK}.{CHECK}"
ACCOUNT_ID = f"/subscriptions/{AZURE_SUBSCRIPTION_ID}/resourceGroups/{RESOURCE_GROUP}/providers/Microsoft.CognitiveServices/accounts/openai1"
def build_account(key_source: str, key_name=None) -> Account:
return Account(
id=ACCOUNT_ID,
name="openai1",
location="eastus",
kind="OpenAI",
public_network_access=False,
disable_local_auth=True,
encryption_key_source=key_source,
encryption_key_name=key_name,
)
def run_check(accounts):
aiservices_client = mock.MagicMock()
aiservices_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME}
aiservices_client.accounts = accounts
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
),
mock.patch(f"{CHECK_PATH}.aiservices_client", new=aiservices_client),
):
from prowler.providers.azure.services.aiservices.aiservices_account_encrypted_with_cmk.aiservices_account_encrypted_with_cmk import (
aiservices_account_encrypted_with_cmk,
)
return aiservices_account_encrypted_with_cmk().execute()
class Test_aiservices_account_encrypted_with_cmk:
def test_no_resources(self):
result = run_check({})
assert len(result) == 0
def test_microsoft_managed_key(self):
result = run_check(
{
AZURE_SUBSCRIPTION_ID: {
ACCOUNT_ID: build_account("Microsoft.CognitiveServices")
}
}
)
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"AI services account openai1 (kind OpenAI) from subscription {AZURE_SUBSCRIPTION_DISPLAY} is encrypted with a Microsoft-managed key."
)
assert result[0].resource_id == ACCOUNT_ID
assert result[0].resource_name == "openai1"
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
def test_customer_managed_key(self):
result = run_check(
{
AZURE_SUBSCRIPTION_ID: {
ACCOUNT_ID: build_account("Microsoft.KeyVault", "cmk1")
}
}
)
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"AI services account openai1 (kind OpenAI) from subscription {AZURE_SUBSCRIPTION_DISPLAY} is encrypted with customer-managed key cmk1."
)
assert result[0].resource_id == ACCOUNT_ID
assert result[0].resource_name == "openai1"
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
def test_key_vault_source_without_key_name(self):
result = run_check(
{
AZURE_SUBSCRIPTION_ID: {
ACCOUNT_ID: build_account("Microsoft.KeyVault", None)
}
}
)
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"AI services account openai1 (kind OpenAI) from subscription {AZURE_SUBSCRIPTION_DISPLAY} is encrypted with a customer-managed key from Azure Key Vault."
)
assert result[0].resource_id == ACCOUNT_ID
assert result[0].resource_name == "openai1"
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
def test_key_vault_source_case_insensitive(self):
result = run_check(
{
AZURE_SUBSCRIPTION_ID: {
ACCOUNT_ID: build_account("Microsoft.Keyvault", "cmk1")
}
}
)
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"AI services account openai1 (kind OpenAI) from subscription {AZURE_SUBSCRIPTION_DISPLAY} is encrypted with customer-managed key cmk1."
)
assert result[0].resource_id == ACCOUNT_ID
assert result[0].resource_name == "openai1"
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
@@ -0,0 +1,74 @@
from unittest import mock
from prowler.providers.azure.services.aiservices.aiservices_service import Account
from tests.providers.azure.azure_fixtures import (
AZURE_SUBSCRIPTION_DISPLAY,
AZURE_SUBSCRIPTION_ID,
AZURE_SUBSCRIPTION_NAME,
RESOURCE_GROUP,
set_mocked_azure_provider,
)
CHECK = "aiservices_account_local_auth_disabled"
CHECK_PATH = f"prowler.providers.azure.services.aiservices.{CHECK}.{CHECK}"
ACCOUNT_ID = f"/subscriptions/{AZURE_SUBSCRIPTION_ID}/resourceGroups/{RESOURCE_GROUP}/providers/Microsoft.CognitiveServices/accounts/foundry1"
def build_account(disable_local_auth: bool) -> Account:
return Account(
id=ACCOUNT_ID,
name="foundry1",
location="swedencentral",
kind="AIServices",
public_network_access=False,
disable_local_auth=disable_local_auth,
encryption_key_source="Microsoft.CognitiveServices",
)
def run_check(accounts):
aiservices_client = mock.MagicMock()
aiservices_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME}
aiservices_client.accounts = accounts
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
),
mock.patch(f"{CHECK_PATH}.aiservices_client", new=aiservices_client),
):
from prowler.providers.azure.services.aiservices.aiservices_account_local_auth_disabled.aiservices_account_local_auth_disabled import (
aiservices_account_local_auth_disabled,
)
return aiservices_account_local_auth_disabled().execute()
class Test_aiservices_account_local_auth_disabled:
def test_no_resources(self):
result = run_check({})
assert len(result) == 0
def test_local_auth_enabled(self):
result = run_check({AZURE_SUBSCRIPTION_ID: {ACCOUNT_ID: build_account(False)}})
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"AI services account foundry1 (kind AIServices) from subscription {AZURE_SUBSCRIPTION_DISPLAY} allows local (API key) authentication."
)
assert result[0].resource_id == ACCOUNT_ID
assert result[0].resource_name == "foundry1"
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
def test_local_auth_disabled(self):
result = run_check({AZURE_SUBSCRIPTION_ID: {ACCOUNT_ID: build_account(True)}})
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"AI services account foundry1 (kind AIServices) from subscription {AZURE_SUBSCRIPTION_DISPLAY} has local (API key) authentication disabled and requires Microsoft Entra ID."
)
assert result[0].resource_id == ACCOUNT_ID
assert result[0].resource_name == "foundry1"
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
@@ -0,0 +1,78 @@
from unittest import mock
from prowler.providers.azure.services.aiservices.aiservices_service import Account
from tests.providers.azure.azure_fixtures import (
AZURE_SUBSCRIPTION_DISPLAY,
AZURE_SUBSCRIPTION_ID,
AZURE_SUBSCRIPTION_NAME,
RESOURCE_GROUP,
set_mocked_azure_provider,
)
CHECK = "aiservices_account_public_network_access_disabled"
CHECK_PATH = f"prowler.providers.azure.services.aiservices.{CHECK}.{CHECK}"
ACCOUNT_ID = f"/subscriptions/{AZURE_SUBSCRIPTION_ID}/resourceGroups/{RESOURCE_GROUP}/providers/Microsoft.CognitiveServices/accounts/openai1"
def build_account(public_network_access: bool) -> Account:
return Account(
id=ACCOUNT_ID,
name="openai1",
location="eastus",
kind="OpenAI",
public_network_access=public_network_access,
disable_local_auth=True,
encryption_key_source="Microsoft.CognitiveServices",
)
def run_check(accounts):
aiservices_client = mock.MagicMock()
aiservices_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME}
aiservices_client.accounts = accounts
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
),
mock.patch(f"{CHECK_PATH}.aiservices_client", new=aiservices_client),
):
from prowler.providers.azure.services.aiservices.aiservices_account_public_network_access_disabled.aiservices_account_public_network_access_disabled import (
aiservices_account_public_network_access_disabled,
)
return aiservices_account_public_network_access_disabled().execute()
class Test_aiservices_account_public_network_access_disabled:
def test_no_resources(self):
result = run_check({})
assert len(result) == 0
def test_no_accounts_in_subscription(self):
assert run_check({AZURE_SUBSCRIPTION_ID: {}}) == []
def test_public_network_access_enabled(self):
result = run_check({AZURE_SUBSCRIPTION_ID: {ACCOUNT_ID: build_account(True)}})
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"AI services account openai1 (kind OpenAI) from subscription {AZURE_SUBSCRIPTION_DISPLAY} has public network access enabled."
)
assert result[0].resource_id == ACCOUNT_ID
assert result[0].resource_name == "openai1"
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
assert result[0].location == "eastus"
def test_public_network_access_disabled(self):
result = run_check({AZURE_SUBSCRIPTION_ID: {ACCOUNT_ID: build_account(False)}})
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"AI services account openai1 (kind OpenAI) from subscription {AZURE_SUBSCRIPTION_DISPLAY} has public network access disabled."
)
assert result[0].resource_id == ACCOUNT_ID
assert result[0].resource_name == "openai1"
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
@@ -0,0 +1,200 @@
from unittest.mock import MagicMock, patch
from prowler.providers.azure.services.aiservices.aiservices_service import (
Account,
AIServices,
)
from tests.providers.azure.azure_fixtures import (
AZURE_SUBSCRIPTION_ID,
RESOURCE_GROUP,
RESOURCE_GROUP_LIST,
set_mocked_azure_provider,
)
ACCOUNT_ID = f"/subscriptions/{AZURE_SUBSCRIPTION_ID}/resourceGroups/{RESOURCE_GROUP}/providers/Microsoft.CognitiveServices/accounts/openai1"
SERVICE_PATH = "prowler.providers.azure.services.aiservices.aiservices_service"
def mock_get_accounts(_):
return {
AZURE_SUBSCRIPTION_ID: {
ACCOUNT_ID: Account(
id=ACCOUNT_ID,
name="openai1",
location="eastus",
kind="OpenAI",
public_network_access=True,
disable_local_auth=False,
encryption_key_source="Microsoft.CognitiveServices",
encryption_key_name=None,
)
}
}
def build_sdk_account(
public_network_access="Disabled",
disable_local_auth=True,
encryption=None,
properties_none=False,
):
account = MagicMock()
account.id = ACCOUNT_ID
account.name = "openai1"
account.location = "eastus"
account.kind = "OpenAI"
if properties_none:
account.properties = None
return account
account.properties.public_network_access = public_network_access
account.properties.disable_local_auth = disable_local_auth
account.properties.encryption = encryption
return account
def build_service(mock_client, resource_groups=None):
with patch(f"{SERVICE_PATH}.AIServices._get_accounts", return_value={}):
aiservices = AIServices(set_mocked_azure_provider())
aiservices.clients = {AZURE_SUBSCRIPTION_ID: mock_client}
aiservices.resource_groups = resource_groups
return aiservices
@patch(f"{SERVICE_PATH}.AIServices._get_accounts", new=mock_get_accounts)
class Test_AIServices_Service:
def test_get_client(self):
aiservices = AIServices(set_mocked_azure_provider())
assert (
aiservices.clients[AZURE_SUBSCRIPTION_ID].__class__.__name__
== "CognitiveServicesManagementClient"
)
def test_accounts_populated(self):
aiservices = AIServices(set_mocked_azure_provider())
account = aiservices.accounts[AZURE_SUBSCRIPTION_ID][ACCOUNT_ID]
assert account.name == "openai1"
assert account.kind == "OpenAI"
class Test_AIServices_get_accounts:
def test_no_resource_groups_lists_subscription(self):
mock_client = MagicMock()
mock_client.accounts.list.return_value = [build_sdk_account()]
aiservices = build_service(mock_client)
result = aiservices._get_accounts()
mock_client.accounts.list.assert_called_once()
mock_client.accounts.list_by_resource_group.assert_not_called()
account = result[AZURE_SUBSCRIPTION_ID][ACCOUNT_ID]
assert account.public_network_access is False
assert account.disable_local_auth is True
assert account.encryption_key_source == "Microsoft.CognitiveServices"
assert account.encryption_key_name is None
def test_single_resource_group(self):
mock_client = MagicMock()
mock_client.accounts.list_by_resource_group.return_value = [build_sdk_account()]
aiservices = build_service(
mock_client, {AZURE_SUBSCRIPTION_ID: [RESOURCE_GROUP]}
)
result = aiservices._get_accounts()
mock_client.accounts.list_by_resource_group.assert_called_once_with(
resource_group_name=RESOURCE_GROUP
)
mock_client.accounts.list.assert_not_called()
assert ACCOUNT_ID in result[AZURE_SUBSCRIPTION_ID]
def test_empty_resource_group_list_skips(self):
mock_client = MagicMock()
aiservices = build_service(mock_client, {AZURE_SUBSCRIPTION_ID: []})
result = aiservices._get_accounts()
mock_client.accounts.list.assert_not_called()
mock_client.accounts.list_by_resource_group.assert_not_called()
assert result[AZURE_SUBSCRIPTION_ID] == {}
def test_multiple_resource_groups(self):
mock_client = MagicMock()
mock_client.accounts.list_by_resource_group.return_value = []
aiservices = build_service(
mock_client, {AZURE_SUBSCRIPTION_ID: RESOURCE_GROUP_LIST}
)
aiservices._get_accounts()
assert mock_client.accounts.list_by_resource_group.call_count == 2
def test_public_network_access_none_treated_as_enabled(self):
mock_client = MagicMock()
mock_client.accounts.list.return_value = [
build_sdk_account(public_network_access=None)
]
aiservices = build_service(mock_client)
account = aiservices._get_accounts()[AZURE_SUBSCRIPTION_ID][ACCOUNT_ID]
assert account.public_network_access is True
def test_disable_local_auth_none_treated_as_enabled_local_auth(self):
mock_client = MagicMock()
mock_client.accounts.list.return_value = [
build_sdk_account(disable_local_auth=None)
]
aiservices = build_service(mock_client)
account = aiservices._get_accounts()[AZURE_SUBSCRIPTION_ID][ACCOUNT_ID]
assert account.disable_local_auth is False
def test_customer_managed_key_mapped(self):
encryption = MagicMock()
encryption.key_source = "Microsoft.KeyVault"
encryption.key_vault_properties.key_name = "cmk1"
mock_client = MagicMock()
mock_client.accounts.list.return_value = [
build_sdk_account(encryption=encryption)
]
aiservices = build_service(mock_client)
account = aiservices._get_accounts()[AZURE_SUBSCRIPTION_ID][ACCOUNT_ID]
assert account.encryption_key_source == "Microsoft.KeyVault"
assert account.encryption_key_name == "cmk1"
def test_properties_none_uses_safe_defaults(self):
mock_client = MagicMock()
mock_client.accounts.list.return_value = [
build_sdk_account(properties_none=True)
]
aiservices = build_service(mock_client)
account = aiservices._get_accounts()[AZURE_SUBSCRIPTION_ID][ACCOUNT_ID]
assert account.public_network_access is True
assert account.disable_local_auth is False
assert account.encryption_key_source == "Microsoft.CognitiveServices"
def test_list_failure_leaves_subscription_empty(self):
mock_client = MagicMock()
mock_client.accounts.list.side_effect = Exception("boom")
aiservices = build_service(mock_client)
result = aiservices._get_accounts()
assert result[AZURE_SUBSCRIPTION_ID] == {}
def test_one_bad_account_does_not_drop_others(self):
bad = build_sdk_account()
bad.id = None # fails Pydantic validation: id must be str
good = build_sdk_account()
mock_client = MagicMock()
mock_client.accounts.list.return_value = [bad, good]
aiservices = build_service(mock_client)
result = aiservices._get_accounts()
assert list(result[AZURE_SUBSCRIPTION_ID]) == [ACCOUNT_ID]
Generated
+16
View File
@@ -972,6 +972,20 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/32/b3/8ec1268082f4d20cc8bf723a1a8e6b9e330bcc338a4dbcee9c7737e9dc1c/azure_mgmt_authorization-4.0.0-py3-none-any.whl", hash = "sha256:d8feeb3842e6ddf1a370963ca4f61fb6edc124e8997b807dd025bc9b2379cd1a", size = 1072620, upload-time = "2023-07-25T04:47:49.26Z" },
]
[[package]]
name = "azure-mgmt-cognitiveservices"
version = "14.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "azure-mgmt-core" },
{ name = "msrest" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/fa/8e/9fcfdd507413a2536c5458b40942705ea8d74fe4e17f05fd1c32bb0225a9/azure_mgmt_cognitiveservices-14.1.0.tar.gz", hash = "sha256:915191374d0adb443863c20aaea2c9f3b9d558a849ac2b78f152249262fdcaf8", size = 184444, upload-time = "2025-10-24T07:28:14.229Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d3/25/ed6bbc01a03991255dd22aa41091360d225f1be20c88710af0f4a6c27459/azure_mgmt_cognitiveservices-14.1.0-py3-none-any.whl", hash = "sha256:3c0eecc00d183842a11d754f4f5c3328ff86f2d6c0a2b81b28dd7e073680258e", size = 290096, upload-time = "2025-10-24T07:28:15.766Z" },
]
[[package]]
name = "azure-mgmt-compute"
version = "34.0.0"
@@ -3789,6 +3803,7 @@ dependencies = [
{ name = "azure-mgmt-apimanagement" },
{ name = "azure-mgmt-applicationinsights" },
{ name = "azure-mgmt-authorization" },
{ name = "azure-mgmt-cognitiveservices" },
{ name = "azure-mgmt-compute" },
{ name = "azure-mgmt-containerregistry" },
{ name = "azure-mgmt-containerservice" },
@@ -3913,6 +3928,7 @@ requires-dist = [
{ name = "azure-mgmt-apimanagement", specifier = "==5.0.0" },
{ name = "azure-mgmt-applicationinsights", specifier = "==4.1.0" },
{ name = "azure-mgmt-authorization", specifier = "==4.0.0" },
{ name = "azure-mgmt-cognitiveservices", specifier = "==14.1.0" },
{ name = "azure-mgmt-compute", specifier = "==34.0.0" },
{ name = "azure-mgmt-containerregistry", specifier = "==12.0.0" },
{ name = "azure-mgmt-containerservice", specifier = "==34.1.0" },