From e224215fa3dbaf398a69622472038410bde6b7f5 Mon Sep 17 00:00:00 2001 From: Hugo966 <148140670+Hugo966@users.noreply.github.com> Date: Wed, 20 Mar 2024 11:11:52 +0100 Subject: [PATCH] feat(azure):App check related with http logs (#3568) Co-authored-by: Sergio Garcia --- .../app/app_http_logs_enabled/__init__.py | 0 .../app_http_logs_enabled.metadata.json | 30 +++ .../app_http_logs_enabled.py | 29 +++ .../azure/services/app/app_service.py | 23 +++ .../azure/services/monitor/monitor_service.py | 8 +- .../app_http_logs_enabled_test.py | 192 ++++++++++++++++++ .../azure/services/app/app_service_test.py | 70 +++++++ 7 files changed, 350 insertions(+), 2 deletions(-) create mode 100644 prowler/providers/azure/services/app/app_http_logs_enabled/__init__.py create mode 100644 prowler/providers/azure/services/app/app_http_logs_enabled/app_http_logs_enabled.metadata.json create mode 100644 prowler/providers/azure/services/app/app_http_logs_enabled/app_http_logs_enabled.py create mode 100644 tests/providers/azure/services/app/app_http_logs_enabled/app_http_logs_enabled_test.py diff --git a/prowler/providers/azure/services/app/app_http_logs_enabled/__init__.py b/prowler/providers/azure/services/app/app_http_logs_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/azure/services/app/app_http_logs_enabled/app_http_logs_enabled.metadata.json b/prowler/providers/azure/services/app/app_http_logs_enabled/app_http_logs_enabled.metadata.json new file mode 100644 index 0000000000..2b58f1c7de --- /dev/null +++ b/prowler/providers/azure/services/app/app_http_logs_enabled/app_http_logs_enabled.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "azure", + "CheckID": "app_http_logs_enabled", + "CheckTitle": "Ensure that logging for Azure AppService 'HTTP logs' is enabled", + "CheckType": [], + "ServiceName": "app", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "low", + "ResourceType": "Microsoft.Web/sites/config", + "Description": "Enable AppServiceHTTPLogs diagnostic log category for Azure App Service instances to ensure all http requests are captured and centrally logged.", + "Risk": "Capturing web requests can be important supporting information for security analysts performing monitoring and incident response activities. Once logging, these logs can be ingested into SIEM or other central aggregation point for the organization.", + "RelatedUrl": "https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-logging-threat-detection#lt-3-enable-logging-for-security-investigation", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "https://docs.bridgecrew.io/docs/ensure-that-app-service-enables-http-logging#terraform" + }, + "Recommendation": { + "Text": "1. Go to App Services For each App Service: 2. Go to Diagnostic Settings 3. Click Add Diagnostic Setting 4. Check the checkbox next to 'HTTP logs' 5. Configure a destination based on your specific logging consumption capability (for example Stream to an event hub and then consuming with SIEM integration for Event Hub logging).", + "Url": "https://docs.microsoft.com/en-us/azure/app-service/troubleshoot-diagnostic-logs" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Log consumption and processing will incur additional cost." +} diff --git a/prowler/providers/azure/services/app/app_http_logs_enabled/app_http_logs_enabled.py b/prowler/providers/azure/services/app/app_http_logs_enabled/app_http_logs_enabled.py new file mode 100644 index 0000000000..6db1697909 --- /dev/null +++ b/prowler/providers/azure/services/app/app_http_logs_enabled/app_http_logs_enabled.py @@ -0,0 +1,29 @@ +from prowler.lib.check.models import Check, Check_Report_Azure +from prowler.providers.azure.services.app.app_client import app_client + + +class app_http_logs_enabled(Check): + def execute(self) -> Check_Report_Azure: + findings = [] + + for subscription_name, apps in app_client.apps.items(): + for app_name, app in apps.items(): + if "functionapp" not in app.kind: + report = Check_Report_Azure(self.metadata()) + report.status = "FAIL" + report.subscription = subscription_name + report.resource_name = app_name + report.resource_id = app.resource_id + if not app.monitor_diagnostic_settings: + report.status_extended = f"App {app_name} does not have a diagnostic setting in subscription {subscription_name}." + else: + for diagnostic_setting in app.monitor_diagnostic_settings: + report.status_extended = f"App {app_name} does not have HTTP Logs enabled in diagnostic setting {diagnostic_setting.name} in subscription {subscription_name}" + for log in diagnostic_setting.logs: + if log.category == "AppServiceHTTPLogs" and log.enabled: + report.status = "PASS" + report.status_extended = f"App {app_name} has HTTP Logs enabled in diagnostic setting {diagnostic_setting.name} in subscription {subscription_name}" + break + findings.append(report) + + return findings diff --git a/prowler/providers/azure/services/app/app_service.py b/prowler/providers/azure/services/app/app_service.py index b11d906bff..10ddfe092d 100644 --- a/prowler/providers/azure/services/app/app_service.py +++ b/prowler/providers/azure/services/app/app_service.py @@ -6,6 +6,8 @@ from azure.mgmt.web.models import ManagedServiceIdentity, SiteConfigResource from prowler.lib.logger import logger from prowler.providers.azure.lib.audit_info.models import Azure_Audit_Info from prowler.providers.azure.lib.service.service import AzureService +from prowler.providers.azure.services.monitor.monitor_client import monitor_client +from prowler.providers.azure.services.monitor.monitor_service import DiagnosticSetting ########################## App @@ -49,8 +51,12 @@ class App(AzureService): getattr(app, "client_cert_enabled", False), getattr(app, "client_cert_mode", "Ignore"), ), + monitor_diagnostic_settings=self.__get_app_monitor_settings__( + app.name, app.resource_group, subscription_name + ), https_only=getattr(app, "https_only", False), identity=getattr(app, "identity", None), + kind=getattr(app, "kind", "app"), ) } ) @@ -78,6 +84,21 @@ class App(AzureService): return cert_mode + def __get_app_monitor_settings__(self, app_name, resource_group, subscription): + logger.info(f"App - Getting monitor diagnostics settings for {app_name}...") + monitor_diagnostics_settings = [] + try: + monitor_diagnostics_settings = monitor_client.diagnostic_settings_with_uri( + self.subscriptions[subscription], + f"subscriptions/{self.subscriptions[subscription]}/resourceGroups/{resource_group}/providers/Microsoft.Web/sites/{app_name}", + monitor_client.clients[subscription], + ) + except Exception as error: + logger.error( + f"Subscription name: {self.subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + return monitor_diagnostics_settings + @dataclass class WebApp: @@ -87,3 +108,5 @@ class WebApp: client_cert_mode: str = "Ignore" auth_enabled: bool = False https_only: bool = False + monitor_diagnostic_settings: list[DiagnosticSetting] = None + kind: str = "app" diff --git a/prowler/providers/azure/services/monitor/monitor_service.py b/prowler/providers/azure/services/monitor/monitor_service.py index 4844277123..547b32b03a 100644 --- a/prowler/providers/azure/services/monitor/monitor_service.py +++ b/prowler/providers/azure/services/monitor/monitor_service.py @@ -42,14 +42,18 @@ class Monitor(AzureService): DiagnosticSetting( id=setting.id, name=setting.id.split("/")[-1], - storage_account_name=setting.storage_account_id.split("/")[-1], + storage_account_name=( + setting.storage_account_id.split("/")[-1] + if getattr(setting, "storage_account_id", None) + else None + ), logs=setting.logs, storage_account_id=setting.storage_account_id, ) ) except Exception as error: logger.error( - f"Subscription name: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + f"Subscription id: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) return diagnostics_settings diff --git a/tests/providers/azure/services/app/app_http_logs_enabled/app_http_logs_enabled_test.py b/tests/providers/azure/services/app/app_http_logs_enabled/app_http_logs_enabled_test.py new file mode 100644 index 0000000000..69531afeb6 --- /dev/null +++ b/tests/providers/azure/services/app/app_http_logs_enabled/app_http_logs_enabled_test.py @@ -0,0 +1,192 @@ +from unittest import mock + +from tests.providers.azure.azure_fixtures import AZURE_SUBSCRIPTION + + +class Test_app_http_logs_enabled: + + def test_app_http_logs_enabled_no_subscriptions(self): + app_client = mock.MagicMock + app_client.apps = {} + + with mock.patch( + "prowler.providers.azure.services.app.app_http_logs_enabled.app_http_logs_enabled.app_client", + new=app_client, + ): + + from prowler.providers.azure.services.app.app_http_logs_enabled.app_http_logs_enabled import ( + app_http_logs_enabled, + ) + + check = app_http_logs_enabled() + result = check.execute() + assert len(result) == 0 + + def test_app_subscriptions_empty(self): + app_client = mock.MagicMock + app_client.apps = {AZURE_SUBSCRIPTION: {}} + + with mock.patch( + "prowler.providers.azure.services.app.app_register_with_identity.app_register_with_identity.app_client", + new=app_client, + ): + from prowler.providers.azure.services.app.app_register_with_identity.app_register_with_identity import ( + app_register_with_identity, + ) + + check = app_register_with_identity() + result = check.execute() + assert len(result) == 0 + + def test_no_diagnostics_settings(self): + app_client = mock.MagicMock() + with mock.patch( + "prowler.providers.azure.services.app.app_http_logs_enabled.app_http_logs_enabled.app_client", + new=app_client, + ): + from prowler.providers.azure.services.app.app_http_logs_enabled.app_http_logs_enabled import ( + app_http_logs_enabled, + ) + from prowler.providers.azure.services.app.app_service import WebApp + + app_client.apps = { + AZURE_SUBSCRIPTION: { + "app1": WebApp( + resource_id="resource_id", + auth_enabled=True, + configurations=None, + client_cert_mode="Ignore", + https_only=False, + identity=None, + kind="webapps", + ) + } + } + + check = app_http_logs_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].resource_name == "app1" + assert result[0].resource_id == "resource_id" + assert ( + result[0].status_extended + == f"App app1 does not have a diagnostic setting in subscription {AZURE_SUBSCRIPTION}." + ) + assert result[0].subscription == AZURE_SUBSCRIPTION + + def test_diagnostic_setting_configured(self): + app_client = mock.MagicMock + + with mock.patch( + "prowler.providers.azure.services.app.app_http_logs_enabled.app_http_logs_enabled.app_client", + new=app_client, + ): + from prowler.providers.azure.services.app.app_http_logs_enabled.app_http_logs_enabled import ( + app_http_logs_enabled, + ) + from prowler.providers.azure.services.app.app_service import WebApp + from prowler.providers.azure.services.monitor.monitor_service import ( + DiagnosticSetting, + ) + + app_client.apps = { + AZURE_SUBSCRIPTION: { + "app_id-1": WebApp( + resource_id="resource_id1", + auth_enabled=True, + configurations=None, + client_cert_mode="Ignore", + https_only=False, + kind="functionapp", + identity=mock.MagicMock, + monitor_diagnostic_settings=[ + DiagnosticSetting( + id="id1/id1", + logs=[ + mock.MagicMock( + category="AppServiceHTTPLogs", + enabled=True, + ), + mock.MagicMock( + category="AppServiceConsoleLogs", + enabled=False, + ), + mock.MagicMock( + category="AppServiceAppLogs", + enabled=True, + ), + mock.MagicMock( + category="AppServiceAuditLogs", + enabled=False, + ), + mock.MagicMock( + category="AppServiceIPSecAuditLogs", + enabled=False, + ), + mock.MagicMock( + category="AppServicePlatformLogs", + enabled=False, + ), + ], + storage_account_name="storage_account_name1", + storage_account_id="storage_account_id1", + name="name_diagnostic_setting1", + ), + ], + ), + "app_id-2": WebApp( + resource_id="resource_id2", + auth_enabled=True, + configurations=None, + client_cert_mode="Ignore", + https_only=False, + kind="WebApp", + identity=mock.MagicMock, + monitor_diagnostic_settings=[ + DiagnosticSetting( + id="id2/id2", + logs=[ + mock.MagicMock( + category="AppServiceHTTPLogs", + enabled=True, + ), + mock.MagicMock( + category="AppServiceConsoleLogs", + enabled=True, + ), + mock.MagicMock( + category="AppServiceAppLogs", + enabled=True, + ), + mock.MagicMock( + category="AppServiceAuditLogs", + enabled=False, + ), + mock.MagicMock( + category="AppServiceIPSecAuditLogs", + enabled=True, + ), + mock.MagicMock( + category="AppServicePlatformLogs", + enabled=False, + ), + ], + storage_account_name="storage_account_name2", + storage_account_id="storage_account_id2", + name="name_diagnostic_setting2", + ), + ], + ), + } + } + check = app_http_logs_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].subscription == AZURE_SUBSCRIPTION + assert result[0].resource_name == "app_id-2" + assert result[0].resource_id == "resource_id2" + assert ( + result[0].status_extended + == f"App app_id-2 has HTTP Logs enabled in diagnostic setting name_diagnostic_setting2 in subscription {AZURE_SUBSCRIPTION}" + ) diff --git a/tests/providers/azure/services/app/app_service_test.py b/tests/providers/azure/services/app/app_service_test.py index 6b5d45728e..59b3351e9f 100644 --- a/tests/providers/azure/services/app/app_service_test.py +++ b/tests/providers/azure/services/app/app_service_test.py @@ -1,8 +1,10 @@ +from unittest import mock from unittest.mock import patch from azure.mgmt.web.models import ManagedServiceIdentity, SiteConfigResource from prowler.providers.azure.services.app.app_service import App, WebApp +from prowler.providers.azure.services.monitor.monitor_service import DiagnosticSetting from tests.providers.azure.azure_fixtures import ( AZURE_SUBSCRIPTION, set_mocked_azure_audit_info, @@ -19,6 +21,40 @@ def mock_app_get_apps(self): auth_enabled=True, client_cert_mode="Required", https_only=True, + monitor_diagnostic_settings=[ + DiagnosticSetting( + id="id2/id2", + logs=[ + mock.MagicMock( + category="AppServiceHTTPLogs", + enabled=False, + ), + mock.MagicMock( + category="AppServiceConsoleLogs", + enabled=True, + ), + mock.MagicMock( + category="AppServiceAppLogs", + enabled=True, + ), + mock.MagicMock( + category="AppServiceAuditLogs", + enabled=False, + ), + mock.MagicMock( + category="AppServiceIPSecAuditLogs", + enabled=True, + ), + mock.MagicMock( + category="AppServicePlatformLogs", + enabled=False, + ), + ], + storage_account_name="storage_account_name2", + storage_account_id="storage_account_id2", + name="name_diagnostic_setting2", + ), + ], ) } } @@ -77,3 +113,37 @@ class Test_App_Service: assert app_service.__get_client_cert_mode__(True, "Optional") == "Allow" assert app_service.__get_client_cert_mode__(True, "Required") == "Required" assert app_service.__get_client_cert_mode__(True, "Foo") == "Ignore" + + def test__get_app_monitor_settings(self): + app_service = App(set_mocked_azure_audit_info()) + assert ( + app_service.apps[AZURE_SUBSCRIPTION]["app_id-1"] + .monitor_diagnostic_settings[0] + .id + == "id2/id2" + ) + assert ( + app_service.apps[AZURE_SUBSCRIPTION]["app_id-1"] + .monitor_diagnostic_settings[0] + .logs[0] + .category + == "AppServiceHTTPLogs" + ) + assert ( + app_service.apps[AZURE_SUBSCRIPTION]["app_id-1"] + .monitor_diagnostic_settings[0] + .storage_account_name + == "storage_account_name2" + ) + assert ( + app_service.apps[AZURE_SUBSCRIPTION]["app_id-1"] + .monitor_diagnostic_settings[0] + .storage_account_id + == "storage_account_id2" + ) + assert ( + app_service.apps[AZURE_SUBSCRIPTION]["app_id-1"] + .monitor_diagnostic_settings[0] + .name + == "name_diagnostic_setting2" + )