mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-24 13:01:56 +00:00
feat(azure): add check to ensure Function Apps redirect HTTP to HTTPS (#11929)
Co-authored-by: Hugo P.Brito <hugopbrit@gmail.com>
This commit is contained in:
@@ -0,0 +1 @@
|
||||
`app_function_ensure_http_is_redirected_to_https` check for Azure provider, verifying that Function Apps enforce HTTPS-only traffic
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"Provider": "azure",
|
||||
"CheckID": "app_function_ensure_http_is_redirected_to_https",
|
||||
"CheckTitle": "Function app redirects HTTP to HTTPS",
|
||||
"CheckType": [],
|
||||
"ServiceName": "app",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "high",
|
||||
"ResourceType": "microsoft.web/sites",
|
||||
"ResourceGroup": "serverless",
|
||||
"Description": "**Azure Function apps** redirect `HTTP` traffic to `HTTPS` when the `HTTPS Only` setting is enabled. This evaluation identifies Function apps that do not force secure transport by checking whether plaintext requests are automatically redirected to encrypted endpoints.",
|
||||
"Risk": "Leaving **HTTP accessible** on a Function app enables **man-in-the-middle** interception, credential and token theft, and response tampering. This undermines **confidentiality** and **integrity**, and can lead to session hijacking or downgrade attacks that bypass TLS.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://learn.microsoft.com/en-us/azure/app-service/configure-ssl-bindings#enforce-https",
|
||||
"https://learn.microsoft.com/en-us/security/benchmark/azure/security-controls-v3-data-protection#dp-3-encrypt-sensitive-data-in-transit"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "az functionapp update --resource-group <RESOURCE_GROUP_NAME> --name <FUNCTION_APP_NAME> --https-only true",
|
||||
"NativeIaC": "```bicep\n// Enable HTTPS-only redirect on an existing Function App\nresource functionApp 'Microsoft.Web/sites@2022-09-01' = {\n name: '<example_resource_name>'\n location: resourceGroup().location\n properties: {\n httpsOnly: true // Critical: forces redirect from HTTP to HTTPS\n }\n}\n```",
|
||||
"Other": "1. Sign in to the Azure portal and go to Function Apps\n2. Select your Function app\n3. Go to TLS/SSL settings and set HTTPS Only to On\n4. Click Save",
|
||||
"Terraform": "```hcl\n# Enforce HTTPS-only on a Function App\nresource \"azurerm_linux_function_app\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n resource_group_name = \"<example_resource_name>\"\n location = \"<example_location>\"\n service_plan_id = \"<example_resource_id>\"\n storage_account_name = \"<example_resource_name>\"\n storage_account_access_key = \"<example_secret_value>\"\n\n https_only = true # Critical: redirects HTTP to HTTPS\n}\n```"
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Enforce **HTTPS-only** for all Function apps.\n- Use trusted certificates and require `TLS 1.2` or later\n- Enable **HSTS** to prevent downgrade/mixed-content\n- Redirect legacy `http` links to `https`\n- Minimize HTTP exposure via WAF/CDN or private access\nApply **defense in depth** to protect data in transit.",
|
||||
"Url": "https://hub.prowler.com/check/app_function_ensure_http_is_redirected_to_https"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"encryption"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": "This check mirrors app_ensure_http_is_redirected_to_https but evaluates Function apps (Microsoft.Web/sites with kind starting with 'functionapp') via app_client.functions. When HTTPS Only is enabled, every incoming HTTP request is redirected to the HTTPS port."
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
from prowler.lib.check.models import Check, Check_Report_Azure
|
||||
from prowler.providers.azure.services.app.app_client import app_client
|
||||
|
||||
|
||||
class app_function_ensure_http_is_redirected_to_https(Check):
|
||||
"""Ensure Function Apps redirect HTTP traffic to HTTPS."""
|
||||
|
||||
def execute(self) -> list[Check_Report_Azure]:
|
||||
"""Execute the check logic.
|
||||
|
||||
Returns:
|
||||
A list of reports for Function Apps HTTPS-only enforcement.
|
||||
"""
|
||||
findings = []
|
||||
|
||||
for (
|
||||
subscription_id,
|
||||
functions,
|
||||
) in app_client.functions.items():
|
||||
subscription_name = app_client.subscriptions.get(
|
||||
subscription_id, subscription_id
|
||||
)
|
||||
for function in functions.values():
|
||||
report = Check_Report_Azure(metadata=self.metadata(), resource=function)
|
||||
report.subscription = subscription_id
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"HTTP is redirected to HTTPS for Function app '{function.name}' in subscription '{subscription_name} ({subscription_id})'."
|
||||
|
||||
if not function.https_only:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"HTTP is not redirected to HTTPS for Function app '{function.name}' in subscription '{subscription_name} ({subscription_id})'."
|
||||
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
@@ -178,6 +178,7 @@ class App(AzureService):
|
||||
ftps_state=getattr(
|
||||
function_config, "ftps_state", None
|
||||
),
|
||||
https_only=getattr(function, "https_only", False),
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -301,3 +302,4 @@ class FunctionApp:
|
||||
public_access: bool
|
||||
vnet_subnet_id: str
|
||||
ftps_state: Optional[str]
|
||||
https_only: bool = False
|
||||
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
from unittest import mock
|
||||
from uuid import uuid4
|
||||
|
||||
from tests.providers.azure.azure_fixtures import (
|
||||
AZURE_SUBSCRIPTION_DISPLAY,
|
||||
AZURE_SUBSCRIPTION_ID,
|
||||
AZURE_SUBSCRIPTION_NAME,
|
||||
set_mocked_azure_provider,
|
||||
)
|
||||
|
||||
|
||||
class Test_app_function_ensure_http_is_redirected_to_https:
|
||||
def test_function_no_subscriptions(self):
|
||||
app_client = mock.MagicMock
|
||||
app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME}
|
||||
app_client.functions = {}
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_azure_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.azure.services.app.app_function_ensure_http_is_redirected_to_https.app_function_ensure_http_is_redirected_to_https.app_client",
|
||||
new=app_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.azure.services.app.app_function_ensure_http_is_redirected_to_https.app_function_ensure_http_is_redirected_to_https import (
|
||||
app_function_ensure_http_is_redirected_to_https,
|
||||
)
|
||||
|
||||
check = app_function_ensure_http_is_redirected_to_https()
|
||||
result = check.execute()
|
||||
assert len(result) == 0
|
||||
|
||||
def test_function_subscriptions_empty(self):
|
||||
app_client = mock.MagicMock
|
||||
app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME}
|
||||
app_client.functions = {AZURE_SUBSCRIPTION_ID: {}}
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_azure_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.azure.services.app.app_function_ensure_http_is_redirected_to_https.app_function_ensure_http_is_redirected_to_https.app_client",
|
||||
new=app_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.azure.services.app.app_function_ensure_http_is_redirected_to_https.app_function_ensure_http_is_redirected_to_https import (
|
||||
app_function_ensure_http_is_redirected_to_https,
|
||||
)
|
||||
|
||||
check = app_function_ensure_http_is_redirected_to_https()
|
||||
result = check.execute()
|
||||
assert len(result) == 0
|
||||
|
||||
def test_function_http_not_redirected(self):
|
||||
resource_id = f"/subscriptions/{uuid4()}"
|
||||
app_client = mock.MagicMock
|
||||
app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME}
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_azure_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.azure.services.app.app_function_ensure_http_is_redirected_to_https.app_function_ensure_http_is_redirected_to_https.app_client",
|
||||
new=app_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.azure.services.app.app_function_ensure_http_is_redirected_to_https.app_function_ensure_http_is_redirected_to_https import (
|
||||
app_function_ensure_http_is_redirected_to_https,
|
||||
)
|
||||
from prowler.providers.azure.services.app.app_service import FunctionApp
|
||||
|
||||
app_client.functions = {
|
||||
AZURE_SUBSCRIPTION_ID: {
|
||||
resource_id: FunctionApp(
|
||||
id=resource_id,
|
||||
name="function-1",
|
||||
location="West Europe",
|
||||
kind="functionapp",
|
||||
function_keys=None,
|
||||
environment_variables=None,
|
||||
identity=None,
|
||||
public_access=True,
|
||||
vnet_subnet_id="",
|
||||
ftps_state="Disabled",
|
||||
https_only=False,
|
||||
)
|
||||
}
|
||||
}
|
||||
check = app_function_ensure_http_is_redirected_to_https()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"HTTP is not redirected to HTTPS for Function app 'function-1' in subscription '{AZURE_SUBSCRIPTION_DISPLAY}'."
|
||||
)
|
||||
assert result[0].resource_name == "function-1"
|
||||
assert result[0].resource_id == resource_id
|
||||
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
|
||||
assert result[0].location == "West Europe"
|
||||
|
||||
def test_function_http_redirected(self):
|
||||
resource_id = f"/subscriptions/{uuid4()}"
|
||||
app_client = mock.MagicMock
|
||||
app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME}
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_azure_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.azure.services.app.app_function_ensure_http_is_redirected_to_https.app_function_ensure_http_is_redirected_to_https.app_client",
|
||||
new=app_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.azure.services.app.app_function_ensure_http_is_redirected_to_https.app_function_ensure_http_is_redirected_to_https import (
|
||||
app_function_ensure_http_is_redirected_to_https,
|
||||
)
|
||||
from prowler.providers.azure.services.app.app_service import FunctionApp
|
||||
|
||||
app_client.functions = {
|
||||
AZURE_SUBSCRIPTION_ID: {
|
||||
resource_id: FunctionApp(
|
||||
id=resource_id,
|
||||
name="function-1",
|
||||
location="West Europe",
|
||||
kind="functionapp",
|
||||
function_keys=None,
|
||||
environment_variables=None,
|
||||
identity=None,
|
||||
public_access=True,
|
||||
vnet_subnet_id="",
|
||||
ftps_state="Disabled",
|
||||
https_only=True,
|
||||
)
|
||||
}
|
||||
}
|
||||
check = app_function_ensure_http_is_redirected_to_https()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"HTTP is redirected to HTTPS for Function app 'function-1' in subscription '{AZURE_SUBSCRIPTION_DISPLAY}'."
|
||||
)
|
||||
assert result[0].resource_name == "function-1"
|
||||
assert result[0].resource_id == resource_id
|
||||
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
|
||||
assert result[0].location == "West Europe"
|
||||
@@ -570,3 +570,38 @@ class Test_App_get_functions:
|
||||
mock_client.web_apps.list_by_resource_group.assert_called_once_with(
|
||||
resource_group_name="RG"
|
||||
)
|
||||
|
||||
def test_get_functions_sets_https_only(self):
|
||||
from prowler.providers.azure.services.app.app_service import App
|
||||
|
||||
mock_client = MagicMock()
|
||||
mock_function = MagicMock()
|
||||
mock_function.id = "/subscriptions/resource_id"
|
||||
mock_function.name = "functionapp-1"
|
||||
mock_function.location = "West Europe"
|
||||
mock_function.kind = "functionapp"
|
||||
mock_function.resource_group = RESOURCE_GROUP
|
||||
mock_function.identity = None
|
||||
mock_function.public_network_access = "Enabled"
|
||||
mock_function.virtual_network_subnet_id = ""
|
||||
mock_function.https_only = True
|
||||
mock_client.web_apps.list.return_value = [mock_function]
|
||||
|
||||
app = object.__new__(App)
|
||||
app.clients = {AZURE_SUBSCRIPTION_ID: mock_client}
|
||||
app.resource_groups = None
|
||||
app._get_function_host_keys = MagicMock(return_value=None)
|
||||
app._list_application_settings = MagicMock(
|
||||
return_value=MagicMock(properties={})
|
||||
)
|
||||
app._get_function_config = MagicMock(
|
||||
return_value=MagicMock(ftps_state="FtpsOnly")
|
||||
)
|
||||
|
||||
result = app._get_functions()
|
||||
|
||||
function = result[AZURE_SUBSCRIPTION_ID][mock_function.id]
|
||||
assert function.https_only is True
|
||||
app._get_function_host_keys.assert_called_once_with(
|
||||
AZURE_SUBSCRIPTION_ID, RESOURCE_GROUP, "functionapp-1"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user