fix(azure): warn on optional function app permission failures (#11922)

This commit is contained in:
Hugo Pereira Brito
2026-07-09 16:23:09 +01:00
committed by GitHub
parent be78fe8374
commit 6f72785a28
14 changed files with 88 additions and 31 deletions
@@ -210,7 +210,9 @@ For more detailed guidance on subscription management and permissions:
The following security checks require the `ProwlerRole` permissions for execution. Ensure the role is assigned to the identity assumed by Prowler before running these checks:
- `app_function_access_keys_configured`
- `app_function_application_insights_enabled`
- `app_function_ftps_deployment_disabled`
- `app_function_latest_runtime_version`
---
@@ -0,0 +1 @@
Azure Function App optional permission failures now log as warnings, and Function App environment variable fields use the correct spelling internally
@@ -14,7 +14,7 @@ class app_function_application_insights_enabled(Check):
subscription_id, subscription_id
)
for function in functions.values():
if function.enviroment_variables is not None:
if function.environment_variables is not None:
report = Check_Report_Azure(
metadata=self.metadata(), resource=function
)
@@ -22,9 +22,9 @@ class app_function_application_insights_enabled(Check):
report.status = "FAIL"
report.status_extended = f"Function {function.name} from subscription {subscription_name} ({subscription_id}) is not using Application Insights."
if function.enviroment_variables.get(
if function.environment_variables.get(
"APPINSIGHTS_INSTRUMENTATIONKEY", None
) or function.enviroment_variables.get(
) or function.environment_variables.get(
"APPLICATIONINSIGHTS_CONNECTION_STRING", None
):
report.status = "PASS"
@@ -14,7 +14,7 @@ class app_function_latest_runtime_version(Check):
subscription_id, subscription_id
)
for function in functions.values():
if function.enviroment_variables is not None:
if function.environment_variables is not None:
report = Check_Report_Azure(
metadata=self.metadata(), resource=function
)
@@ -23,13 +23,13 @@ class app_function_latest_runtime_version(Check):
report.status_extended = f"Function {function.name} from subscription {subscription_name} ({subscription_id}) is using the latest runtime."
if (
function.enviroment_variables.get(
function.environment_variables.get(
"FUNCTIONS_EXTENSION_VERSION", ""
)
!= "~4"
):
report.status = "FAIL"
report.status_extended = f"Function {function.name} from subscription {subscription_name} ({subscription_id}) is not using the latest runtime. The current runtime is '{function.enviroment_variables.get('FUNCTIONS_EXTENSION_VERSION', '')}' and should be '~4'."
report.status_extended = f"Function {function.name} from subscription {subscription_name} ({subscription_id}) is not using the latest runtime. The current runtime is '{function.environment_variables.get('FUNCTIONS_EXTENSION_VERSION', '')}' and should be '~4'."
findings.append(report)
@@ -158,7 +158,7 @@ class App(AzureService):
location=function.location,
kind=function.kind,
function_keys=function_keys,
enviroment_variables=getattr(
environment_variables=getattr(
application_settings, "properties", None
),
identity=getattr(function, "identity", None),
@@ -225,7 +225,7 @@ class App(AzureService):
name=name,
)
except Exception as error:
logger.error(
logger.warning(
f"Error getting host keys for {name} in {resource_group}: {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
return None
@@ -249,7 +249,7 @@ class App(AzureService):
name=name,
)
except Exception as error:
logger.error(
logger.warning(
f"Error getting application settings for {name} in {resource_group}: {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
return None
@@ -296,7 +296,7 @@ class FunctionApp:
location: str
kind: str
function_keys: Optional[Dict[str, str]]
enviroment_variables: Optional[Dict[str, str]]
environment_variables: Optional[Dict[str, str]]
identity: ManagedServiceIdentity
public_access: bool
vnet_subnet_id: str
@@ -87,7 +87,7 @@ class Test_app_function_access_keys_configured:
location="West Europe",
kind="functionapp,linux",
function_keys={},
enviroment_variables={},
environment_variables={},
identity=None,
public_access=False,
vnet_subnet_id=None,
@@ -143,7 +143,7 @@ class Test_app_function_access_keys_configured:
"default": "key1",
"key2": "key2",
},
enviroment_variables={},
environment_variables={},
identity=None,
public_access=False,
vnet_subnet_id=None,
@@ -87,7 +87,7 @@ class Test_app_function_application_insights_enabled:
location="West Europe",
kind="functionapp,linux",
function_keys={},
enviroment_variables={},
environment_variables={},
identity=None,
public_access=False,
vnet_subnet_id=None,
@@ -138,7 +138,9 @@ class Test_app_function_application_insights_enabled:
location="West Europe",
kind="functionapp,linux",
function_keys={},
enviroment_variables={"APPINSIGHTS_INSTRUMENTATIONKEY": "1234"},
environment_variables={
"APPINSIGHTS_INSTRUMENTATIONKEY": "1234"
},
identity=None,
public_access=False,
vnet_subnet_id=None,
@@ -189,7 +191,9 @@ class Test_app_function_application_insights_enabled:
location="West Europe",
kind="functionapp,linux",
function_keys={},
enviroment_variables={"APPINSIGHTS_INSTRUMENTATIONKEY": "1234"},
environment_variables={
"APPINSIGHTS_INSTRUMENTATIONKEY": "1234"
},
identity=None,
public_access=False,
vnet_subnet_id=None,
@@ -240,7 +244,7 @@ class Test_app_function_application_insights_enabled:
location="West Europe",
kind="functionapp,linux",
function_keys={},
enviroment_variables={},
environment_variables={},
identity=None,
public_access=False,
vnet_subnet_id=None,
@@ -87,7 +87,7 @@ class Test_app_function_ftps_deployment_disabled:
location="West Europe",
kind="functionapp,linux",
function_keys={},
enviroment_variables={},
environment_variables={},
identity=mock.MagicMock(type="SystemAssigned"),
public_access=False,
vnet_subnet_id=None,
@@ -138,7 +138,7 @@ class Test_app_function_ftps_deployment_disabled:
location="West Europe",
kind="functionapp,linux",
function_keys={},
enviroment_variables={},
environment_variables={},
identity=mock.MagicMock(type="SystemAssigned"),
public_access=False,
vnet_subnet_id=None,
@@ -189,7 +189,7 @@ class Test_app_function_ftps_deployment_disabled:
location="West Europe",
kind="functionapp,linux",
function_keys={},
enviroment_variables={},
environment_variables={},
identity=mock.MagicMock(type="SystemAssigned"),
public_access=False,
vnet_subnet_id=None,
@@ -87,7 +87,7 @@ class Test_app_function_identity_is_configured:
location="West Europe",
kind="functionapp,linux",
function_keys={},
enviroment_variables={},
environment_variables={},
identity=None,
public_access=False,
vnet_subnet_id=None,
@@ -138,7 +138,7 @@ class Test_app_function_identity_is_configured:
location="West Europe",
kind="functionapp,linux",
function_keys={},
enviroment_variables={},
environment_variables={},
identity=mock.MagicMock(type="SystemAssigned"),
public_access=False,
vnet_subnet_id=None,
@@ -88,7 +88,7 @@ class Test_app_function_identity_without_admin_privileges:
location="West Europe",
kind="functionapp,linux",
function_keys={},
enviroment_variables={},
environment_variables={},
identity=None,
public_access=False,
vnet_subnet_id=None,
@@ -140,7 +140,7 @@ class Test_app_function_identity_without_admin_privileges:
location="West Europe",
kind="functionapp,linux",
function_keys={},
enviroment_variables={},
environment_variables={},
identity=mock.MagicMock(principal_id="123"),
public_access=False,
vnet_subnet_id=None,
@@ -228,7 +228,7 @@ class Test_app_function_identity_without_admin_privileges:
location="West Europe",
kind="functionapp,linux",
function_keys={},
enviroment_variables={},
environment_variables={},
identity=mock.MagicMock(principal_id="123"),
public_access=False,
vnet_subnet_id=None,
@@ -87,7 +87,7 @@ class Test_app_function_latest_runtime_version:
location="West Europe",
kind="functionapp,linux",
function_keys={},
enviroment_variables={"FUNCTIONS_EXTENSION_VERSION": "~4"},
environment_variables={"FUNCTIONS_EXTENSION_VERSION": "~4"},
identity=None,
public_access=False,
vnet_subnet_id=None,
@@ -137,7 +137,7 @@ class Test_app_function_latest_runtime_version:
location="West Europe",
kind="functionapp,linux",
function_keys={},
enviroment_variables={"FUNCTIONS_EXTENSION_VERSION": "2"},
environment_variables={"FUNCTIONS_EXTENSION_VERSION": "2"},
identity=None,
public_access=False,
vnet_subnet_id=None,
@@ -87,7 +87,7 @@ class Test_app_function_not_publicly_accessible:
location="West Europe",
kind="functionapp,linux",
function_keys={},
enviroment_variables={},
environment_variables={},
identity=mock.MagicMock(type="SystemAssigned"),
public_access=False,
vnet_subnet_id=None,
@@ -138,7 +138,7 @@ class Test_app_function_not_publicly_accessible:
location="West Europe",
kind="functionapp,linux",
function_keys={},
enviroment_variables={},
environment_variables={},
identity=mock.MagicMock(type="SystemAssigned"),
public_access=True,
vnet_subnet_id=None,
@@ -87,7 +87,7 @@ class Test_app_function_vnet_integration_enabled:
location="West Europe",
kind="functionapp,linux",
function_keys={},
enviroment_variables={},
environment_variables={},
identity=None,
public_access=True,
vnet_subnet_id="vnet_subnet_id",
@@ -138,7 +138,7 @@ class Test_app_function_vnet_integration_enabled:
location="West Europe",
kind="functionapp,linux",
function_keys={},
enviroment_variables={},
environment_variables={},
identity=None,
public_access=True,
vnet_subnet_id=None,
@@ -222,7 +222,7 @@ class Test_App_Service:
location="West Europe",
kind="functionapp",
function_keys=None,
enviroment_variables=None,
environment_variables=None,
identity=ManagedServiceIdentity(type="SystemAssigned"),
public_access=True,
vnet_subnet_id="",
@@ -247,6 +247,56 @@ class Test_App_Service:
== "functionapp-1"
)
def test_get_function_host_keys_logs_warning_on_optional_failure(self):
from prowler.providers.azure.services.app.app_service import App
mock_client = MagicMock()
mock_client.web_apps.list_host_keys.side_effect = Exception("Forbidden")
app = object.__new__(App)
app.clients = {AZURE_SUBSCRIPTION_ID: mock_client}
with (
patch(
"prowler.providers.azure.services.app.app_service.logger.warning"
) as mock_warning,
patch(
"prowler.providers.azure.services.app.app_service.logger.error"
) as mock_error,
):
result = app._get_function_host_keys(
AZURE_SUBSCRIPTION_ID, RESOURCE_GROUP, "functionapp-1"
)
assert result is None
mock_warning.assert_called_once()
mock_error.assert_not_called()
def test_list_application_settings_logs_warning_on_optional_failure(self):
from prowler.providers.azure.services.app.app_service import App
mock_client = MagicMock()
mock_client.web_apps.list_application_settings.side_effect = Exception(
"Forbidden"
)
app = object.__new__(App)
app.clients = {AZURE_SUBSCRIPTION_ID: mock_client}
with (
patch(
"prowler.providers.azure.services.app.app_service.logger.warning"
) as mock_warning,
patch(
"prowler.providers.azure.services.app.app_service.logger.error"
) as mock_error,
):
result = app._list_application_settings(
AZURE_SUBSCRIPTION_ID, RESOURCE_GROUP, "functionapp-1"
)
assert result is None
mock_warning.assert_called_once()
mock_error.assert_not_called()
class Test_App_get_apps:
def test_get_apps_no_resource_groups(self):