diff --git a/.script/tests/KqlvalidationsTests/CustomTables/IntegrationTable_CL.json b/.script/tests/KqlvalidationsTests/CustomTables/IntegrationTable_CL.json new file mode 100644 index 00000000000..5a374ae0890 --- /dev/null +++ b/.script/tests/KqlvalidationsTests/CustomTables/IntegrationTable_CL.json @@ -0,0 +1,61 @@ +{ + "Name": "IntegrationTable_CL", + "Properties": [ + { + "name": "TimeGenerated", + "type": "datetime" + }, + { + "name": "typeName", + "type": "string" + }, + { + "name": "objectName", + "type": "string" + }, + { + "name": "networkCommunication", + "type": "dynamic" + }, + { + "name": "customUuid", + "type": "string" + }, + { + "name": "objectTypeName", + "type": "string" + }, + { + "name": "occurTime", + "type": "string" + }, + { + "name": "displayName", + "type": "string" + }, + { + "name": "responses", + "type": "dynamic" + }, + { + "name": "objectHashSha1", + "type": "string" + }, + { + "name": "severityLevel", + "type": "string" + }, + { + "name": "category", + "type": "string" + }, + { + "name": "objectUrl", + "type": "string" + }, + { + "name": "context", + "type": "dynamic" + } + ] +} \ No newline at end of file diff --git a/Solutions/ESET Protect Platform/Data Connectors/ESETProtectPlatform_API_FunctionApp.json b/Solutions/ESET Protect Platform/Data Connectors/ESETProtectPlatform_API_FunctionApp.json new file mode 100644 index 00000000000..fa05bc9d046 --- /dev/null +++ b/Solutions/ESET Protect Platform/Data Connectors/ESETProtectPlatform_API_FunctionApp.json @@ -0,0 +1,80 @@ +{ + "id": "ESETProtectPlatform", + "title": "ESET Protect Platform", + "publisher": "ESET", + "descriptionMarkdown": "The ESET Protect Platform data connector enables users to inject detections data from [ESET Protect Platform](https://www.eset.com/int/business/protect-platform/) using the provided [Integration REST API](https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/ESET%20Protect%20Platform/Data%20Connectors). Integration REST API runs as scheduled Azure Function App.", + "graphQueries": [{"metricName": "Total data received", "legend": "IntegrationTable_CL", "baseQuery": "IntegrationTable_CL"}], + "sampleQueries": [ + {"description": "All table records sorted by time", "query": "IntegrationTable_CL\n| sort by TimeGenerated desc"} + ], + "dataTypes": [ + { + "name": "IntegrationTable_CL", + "lastDataReceivedQuery": "IntegrationTable_CL\n | summarize Time = max(TimeGenerated)\n | where isnotempty(Time)" + } + ], + "connectivityCriterias": [ + { + "type": "IsConnectedQuery", + "value": [ + "IntegrationTable_CL\n | summarize LastLogReceived = max(TimeGenerated)\n | project IsConnected = LastLogReceived > ago(30d)" + ] + } + ], + "availability": {"status": 1, "isPreview": false}, + "permissions": { + "resourceProvider": [ + { + "provider": "Microsoft.OperationalInsights/workspaces", + "permissionsDisplayText": "read and write permissions on the workspace are required.", + "providerDisplayName": "Workspace", + "scope": "Workspace", + "requiredPermissions": { + "write": true, + "read": true, + "delete": true + } + }, + { + "provider": "Microsoft.OperationalInsights/workspaces/sharedKeys", + "permissionsDisplayText": "read permissions to shared keys for the workspace are required. [See the documentation to learn more about workspace keys](https://docs.microsoft.com/azure/azure-monitor/platform/agent-windows#obtain-workspace-id-and-key).", + "providerDisplayName": "Keys", + "scope": "Workspace", + "requiredPermissions": { + "action": true + } + } + ], + "customs": [ + { + "name": "Microsoft.Web/sites permissions", + "description": "Read and write permissions to Azure Functions to create a Function App is required. [See the documentation to learn more about Azure Functions](https://docs.microsoft.com/azure/azure-functions/)." + }, + { + "name": "Permission to register an application in Microsoft Entra ID", + "description": "Sufficient permissions to register an application with your Microsoft Entra tenant are required." + }, + { + "name": "Permission to assign a role to the registered application", + "description": "Permission to assign the Monitoring Metrics Publisher role to the registered application in Microsoft Entra ID is required." + } + ] + }, + "instructionSteps": [ + { + "description": ">**NOTE:** The ESET Protect Platform data connector uses Azure Functions to connect to the ESET Protect Platform via Eset Connect API to pull detections logs into Microsoft Sentinel. This process might result in additional data ingestion costs. See details on the [Azure Functions pricing page](https://azure.microsoft.com/pricing/details/functions/)." + }, + { + "title": "Step 1 - Create an API user", + "description": "Use this [instruction](https://help.eset.com/eset_connect/en-US/create_api_user_account.html) to create an ESET Connect API User account with **Login** and **Password**." + }, + { + "title": "Step 2 - Create a registered application", + "description": "Create a Microsoft Entra ID registered application by following the steps in the [Register a new application instruction.](https://learn.microsoft.com/en-us/azure/healthcare-apis/register-application#register-a-new-application)" + }, + { + "title": "Step 3 - Deploy the ESET Protect Platform data connector using the Azure Resource Manager (ARM) template", + "description": "\n\n1. Click the **Deploy to Azure** button below. \n\n\t[![Deploy To Azure](https://aka.ms/deploytoazurebutton)](https://aka.ms/sentinel-EsetProtectionPlatform-azuredeploy)\n\n2. Select the name of the **Log Analytics workspace** associated with your Microsoft Sentinel. Select the same **Resource Group** as the Resource Group of the Log Analytics workspace.\n\n3. Type the parameters of the registered application in Microsoft Entra ID: **Azure Client ID**, **Azure Client Secret**, **Azure Tenant ID**, **Object ID**. You can find the **Object ID** on Azure Portal by following this path \n> Microsoft Entra ID -> Manage (on the left-side menu) -> Enterprise applications -> Object ID column (the value next to your registered application name).\n\n4. Provide the ESET Connect API user account **Login** and **Password** obtained in **Step 1**." + } + ] +} diff --git a/Solutions/ESET Protect Platform/Data Connectors/FunctionAppESETProtectPlatform.zip b/Solutions/ESET Protect Platform/Data Connectors/FunctionAppESETProtectPlatform.zip new file mode 100644 index 00000000000..dbcc365aed0 Binary files /dev/null and b/Solutions/ESET Protect Platform/Data Connectors/FunctionAppESETProtectPlatform.zip differ diff --git a/Solutions/ESET Protect Platform/Data Connectors/azuredeploy_ESETProtectPlatform_API_FunctionApp.json b/Solutions/ESET Protect Platform/Data Connectors/azuredeploy_ESETProtectPlatform_API_FunctionApp.json new file mode 100644 index 00000000000..f39e2ca2a64 --- /dev/null +++ b/Solutions/ESET Protect Platform/Data Connectors/azuredeploy_ESETProtectPlatform_API_FunctionApp.json @@ -0,0 +1,503 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-08-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "workspaceName": { + "type": "string", + "metadata": { + "description": "The name of the Log Analytics workspace associated with Microsoft Sentinel." + } + }, + "tableName": { + "type": "string", + "metadata": { + "description": "The name of the custom Log Analytics table to be created." + }, + "defaultValue": "IntegrationTable_CL" + }, + "dataCollectionEndpointName": { + "type": "string", + "metadata": { + "description": "The name of the Data Collection Endpoint to be created." + }, + "defaultValue": "integrationDCE" + }, + "dataCollectionRuleName": { + "type": "string", + "metadata": { + "description": "The name of the Data Collection Rule to be created." + }, + "defaultValue": "integrationDCR" + }, + "applicationName": { + "type": "string", + "metadata": { + "description": "The name of the Azure Function App to be created." + } + }, + "applicationRunInterval": { + "type": "int", + "defaultValue": 5, + "allowedValues": [ + 5, + 10, + 15 + ], + "metadata": { + "description": "The interval in minutes of sending detections to Microsoft Sentinel e.g. every 5 minutes." + } + }, + "objectID": { + "type": "string", + "metadata": { + "description": "The Object ID of the Service Principal associated with the registered application in Microsoft Entra ID." + } + }, + "azureClientID": { + "type": "string", + "metadata": { + "description": "The Azure Client ID of the registered application in Microsoft Entra ID." + } + }, + "azureClientSecret": { + "type": "secureString", + "metadata": { + "description": "The Azure Client Secret of the registered application in Microsoft Entra ID." + } + }, + "azureTenantID": { + "type": "string", + "metadata": { + "description": "The Azure Tenant ID of the registered application in Microsoft Entra ID." + } + }, + "login": { + "type": "string", + "metadata": { + "description": "The ESET Connect API user account login." + } + }, + "password": { + "type": "secureString", + "metadata": { + "description": "The ESET Connect API user account password." + } + }, + "instanceRegion": { + "type": "string", + "defaultValue": "eu", + "allowedValues": [ + "eu", + "us", + "jpn", + "ca", + "de" + ], + "metadata": { + "description": "The region where your ESET Protect/Inspect/ECOS instance is running." + } + }, + "keyBase": { + "type": "string", + "defaultValue": "[newGuid()]", + "metadata": { + "description": "Do not change this value. Base string for the key to encrypt/decrypt token data." + } + } + }, + "variables": { + "tableNameCL": "[if(endsWith(parameters('tableName'), '_CL'), parameters('tableName'), concat(parameters('tableName'), '_CL'))]", + "customTableName": "[concat('Custom-', variables('tableNameCL'))]", + "applicationName": "[concat(parameters('applicationName'), uniquestring(resourceGroup().id))]", + "dataCollectionEndpointId":"[resourceId('Microsoft.Insights/dataCollectionEndpoints', parameters('dataCollectionEndpointName'))]", + "dataCollectionRuleId": "[resourceId('Microsoft.Insights/dataCollectionRules', parameters('dataCollectionRuleName'))]", + "location": "[resourceGroup().location]", + "hostingPlanName": "[variables('applicationName')]", + "contentShare": "[variables('applicationName')]", + "storageAccountName": "[concat(uniquestring(resourceGroup().id), 'azfunction')]", + "workspaces_integration_log_analytics_workspace_externalid":"[resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspaceName'))]", + "keyBase64": "[base64(replace(parameters('keyBase'), '-', ''))]" + + }, + "resources": [ + { + "type": "Microsoft.Insights/dataCollectionEndpoints", + "name": "[parameters('dataCollectionEndpointName')]", + "location": "[variables('location')]", + "dependsOn": [ + "[resourceId('Microsoft.OperationalInsights/workspaces/tables', parameters('workspaceName'), variables('tableNameCL'))]" + ], + "apiVersion": "2021-04-01", + "properties": { + "networkAcls": { + "publicNetworkAccess": "Enabled" + } + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/tables", + "apiVersion": "2022-10-01", + "name": "[concat(parameters('workspaceName'), '/', variables('tableNameCL'))]", + "location": "[variables('location')]", + "properties": { + "schema": { + "name": "[variables('tableNameCL')]", + "columns": [ + { + "name": "TimeGenerated", + "type": "datetime" + }, + { + "name": "typeName", + "type": "string" + }, + { + "name": "objectName", + "type": "string" + }, + { + "name": "networkCommunication", + "type": "dynamic" + }, + { + "name": "customUuid", + "type": "string" + }, + { + "name": "objectTypeName", + "type": "string" + }, + { + "name": "occurTime", + "type": "string" + }, + { + "name": "displayName", + "type": "string" + }, + { + "name": "responses", + "type": "dynamic" + }, + { + "name": "objectHashSha1", + "type": "string" + }, + { + "name": "severityLevel", + "type": "string" + }, + { + "name": "category", + "type": "string" + }, + { + "name": "objectUrl", + "type": "string" + }, + { + "name": "context", + "type": "dynamic" + } + ] + } + } + }, + { + "type": "Microsoft.Insights/dataCollectionRules", + "apiVersion": "2023-03-11", + "name": "[parameters('dataCollectionRuleName')]", + "location": "[variables('location')]", + "dependsOn": [ + "[resourceId('Microsoft.OperationalInsights/workspaces/tables', parameters('workspaceName'), variables('tableNameCL'))]", + "[variables('dataCollectionEndpointId')]" + ], + "properties": { + "dataCollectionEndpointId": "[variables('dataCollectionEndpointId')]", + "streamDeclarations": { + "[variables('customTableName')]": { + "columns": [ + { + "name": "TimeGenerated", + "type": "datetime" + }, + { + "name": "typeName", + "type": "string" + }, + { + "name": "objectName", + "type": "string" + }, + { + "name": "networkCommunication", + "type": "dynamic" + }, + { + "name": "customUuid", + "type": "string" + }, + { + "name": "objectTypeName", + "type": "string" + }, + { + "name": "occurTime", + "type": "string" + }, + { + "name": "displayName", + "type": "string" + }, + { + "name": "responses", + "type": "dynamic" + }, + { + "name": "objectHashSha1", + "type": "string" + }, + { + "name": "severityLevel", + "type": "string" + }, + { + "name": "category", + "type": "string" + }, + { + "name": "objectUrl", + "type": "string" + }, + { + "name": "context", + "type": "dynamic" + } + ] + } + }, + "dataSources": {}, + "destinations": { + "logAnalytics": [ + { + "workspaceResourceId": "[variables('workspaces_integration_log_analytics_workspace_externalid')]", + "name": "[parameters('workspaceName')]" + } + ] + }, + "dataFlows": [ + { + "streams": [ + "[variables('customTableName')]" + ], + "destinations": [ + "[parameters('workspaceName')]" + ], + "transformKql": "source", + "outputStream": "[variables('customTableName')]" + } + ] + } + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "name": "[guid(parameters('dataCollectionRuleName'), parameters('objectID'), 'Monitoring Metrics Publisher')]", + "scope": "[variables('dataCollectionRuleId')]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '3913510d-42f4-4e42-8a64-420c390055eb')]", + "principalId": "[parameters('objectID')]", + "principalType": "ServicePrincipal" + }, + "dependsOn": ["[variables('dataCollectionRuleId')]"] + }, + { + "apiVersion": "2022-03-01", + "name": "[variables('applicationName')]", + "type": "Microsoft.Web/sites", + "kind": "functionapp,linux", + "location": "[resourceGroup().location]", + "tags": {}, + "dependsOn": [ + "[concat('Microsoft.Web/serverfarms/', variables('hostingPlanName'))]", + "[concat('Microsoft.Storage/storageAccounts/', variables('storageAccountName'))]", + "[variables('dataCollectionRuleId')]" + + ], + "properties": { + "name": "[variables('applicationName')]", + "siteConfig": { + "appSettings": [ + { + "name": "FUNCTIONS_EXTENSION_VERSION", + "value": "~4" + }, + { + "name": "FUNCTIONS_WORKER_RUNTIME", + "value": "python" + }, + { + "name": "APPLICATIONINSIGHTS_CONNECTION_STRING", + "value": "[reference(concat('microsoft.insights/components/', variables('applicationName')), '2015-05-01').ConnectionString]" + }, + { + "name": "AzureWebJobsStorage", + "value": "[concat('DefaultEndpointsProtocol=https;AccountName=',variables('storageAccountName'),';AccountKey=',listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2019-06-01').keys[0].value,';EndpointSuffix=','core.windows.net')]" + }, + { + "name": "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING", + "value": "[concat('DefaultEndpointsProtocol=https;AccountName=',variables('storageAccountName'),';AccountKey=',listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2019-06-01').keys[0].value,';EndpointSuffix=','core.windows.net')]" + }, + { + "name": "WEBSITE_CONTENTSHARE", + "value": "[toLower(variables('contentShare'))]" + }, + { + "name": "WEBSITE_RUN_FROM_PACKAGE", + "value": "https://aka.ms/sentinel-EsetProtectionPlatform-FunctionApp" + }, + { + "name": "ENDPOINT_URI", + "value": "[reference(variables('dataCollectionEndpointId'), '2021-04-01').logsIngestion.endpoint]" + }, + { + "name": "DCR_IMMUTABLEID", + "value": "[reference(variables('dataCollectionRuleId'), '2023-03-11').immutableId]" + }, + { + "name": "STREAM_NAME", + "value": "[variables('customTableName')]" + }, + { + "name": "AZURE_CLIENT_ID", + "value": "[parameters('azureClientID')]" + }, + { + "name": "AZURE_CLIENT_SECRET", + "value": "[parameters('azureClientSecret')]" + }, + { + "name": "AZURE_TENANT_ID", + "value": "[parameters('azureTenantID')]" + }, + { + "name": "PASSWORD_INTEGRATION", + "value": "[parameters('password')]" + }, + { + "name": "USERNAME_INTEGRATION", + "value": "[parameters('login')]" + }, + { + "name": "INTERVAL", + "value": "[parameters('applicationRunInterval')]" + }, + { + "name": "PYTHONPATH", + "value": "/home/site/wwwroot/.python_packages/lib/site-packages,/home/site/wwwroot" + }, + { + "name": "PYTHON_ISOLATE_WORKER_DEPENDENCIES", + "value": "1" + }, + { + "name": "KEY_BASE64", + "value": "[variables('keyBase64')]" + }, + { + + "name": "INSTANCE_REGION", + "value": "[parameters('instanceRegion')]" + } + ], + "cors": { + "allowedOrigins": [ + "https://portal.azure.com" + ] + }, + "use32BitWorkerProcess": false, + "ftpsState": "FtpsOnly", + "linuxFxVersion": "Python|3.11" + }, + "clientAffinityEnabled": false, + "publicNetworkAccess": "Enabled", + "httpsOnly": true, + "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]" + }, + "resources": [ + { + "type": "Microsoft.Web/sites/basicPublishingCredentialsPolicies", + "apiVersion": "2022-09-01", + "name": "[concat(variables('applicationName'), '/scm')]", + "properties": { + "allow": false + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/Sites', variables('applicationName'))]" + ] + }, + { + "type": "Microsoft.Web/sites/basicPublishingCredentialsPolicies", + "apiVersion": "2022-09-01", + "name": "[concat(variables('applicationName'), '/ftp')]", + "properties": { + "allow": false + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/Sites', variables('applicationName'))]" + ] + } + ] + }, + { + "apiVersion": "2022-03-01", + "name": "[variables('hostingPlanName')]", + "type": "Microsoft.Web/serverfarms", + "location": "[resourceGroup().location]", + "kind": "linux", + "tags": {}, + "dependsOn": [], + "properties": { + "name": "[variables('hostingPlanName')]", + "workerSize": "0", + "workerSizeId": "0", + "numberOfWorkers": 1, + "reserved": true + }, + "sku": { + "Tier": "Dynamic", + "Name": "Y1" + } + }, + { + "apiVersion": "2020-02-02", + "name": "[variables('applicationName')]", + "type": "microsoft.insights/components", + "location": "westeurope", + "tags": {}, + "dependsOn": [], + "properties": { + "ApplicationId": "[variables('applicationName')]", + "Request_Source": "IbizaWebAppExtensionCreate", + "Flow_Type": "Redfield", + "Application_Type": "web", + "WorkspaceResourceId": "[variables('workspaces_integration_log_analytics_workspace_externalid')]" + } + }, + { + "apiVersion": "2022-05-01", + "type": "Microsoft.Storage/storageAccounts", + "name": "[variables('storageAccountName')]", + "location": "[resourceGroup().location]", + "tags": {}, + "sku": { + "name": "Standard_LRS" + }, + "properties": { + "supportsHttpsTrafficOnly": true, + "minimumTlsVersion": "TLS1_2", + "defaultToOAuthAuthentication": true + } + } + ] +} \ No newline at end of file diff --git a/Solutions/ESET Protect Platform/Data Connectors/function_app.py b/Solutions/ESET Protect Platform/Data Connectors/function_app.py new file mode 100644 index 00000000000..37b484f31cc --- /dev/null +++ b/Solutions/ESET Protect Platform/Data Connectors/function_app.py @@ -0,0 +1,24 @@ +import logging +import os + +import azure.functions as func + +app = func.FunctionApp() + + +@app.timer_trigger( + schedule=f"0 */{os.getenv('INTERVAL', 5)} * * * *", arg_name="myTimer", run_on_startup=False, use_monitor=False +) +def timer_trigger(myTimer: func.TimerRequest) -> None: + if myTimer.past_due: + logging.info("The timer is past due!") + + logging.info("MAIN execution") + try: + from integration.main import main + + main() + except Exception as e: + logging.error(f"main error: {e}") + + logging.info("Python timer trigger function executed.") diff --git a/Solutions/ESET Protect Platform/Data Connectors/host.json b/Solutions/ESET Protect Platform/Data Connectors/host.json new file mode 100644 index 00000000000..9df913614d9 --- /dev/null +++ b/Solutions/ESET Protect Platform/Data Connectors/host.json @@ -0,0 +1,15 @@ +{ + "version": "2.0", + "logging": { + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request" + } + } + }, + "extensionBundle": { + "id": "Microsoft.Azure.Functions.ExtensionBundle", + "version": "[4.*, 5.0.0)" + } +} \ No newline at end of file diff --git a/Solutions/ESET Protect Platform/Data Connectors/integration/__init__.py b/Solutions/ESET Protect Platform/Data Connectors/integration/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/Solutions/ESET Protect Platform/Data Connectors/integration/exceptions.py b/Solutions/ESET Protect Platform/Data Connectors/integration/exceptions.py new file mode 100644 index 00000000000..f08735288c7 --- /dev/null +++ b/Solutions/ESET Protect Platform/Data Connectors/integration/exceptions.py @@ -0,0 +1,41 @@ +import logging + + +class AuthenticationException(Exception): + def __init__(self, status: int, message: str) -> None: + self.status = status + self.message = message + self.s = f"AuthenticationException happend with status: {self.status}. Message: {self.message}" + logging.error(self.s) + + def __str__(self) -> str: + return self.s + + +class MissingCredentialsException(Exception): + def __init__(self) -> None: + self.s = "Missing credentials. Check if username and password are passed and correct." + logging.error(self.s) + + def __str__(self) -> str: + return self.s + + +class InvalidCredentialsException(AuthenticationException): + def __init__(self, e: AuthenticationException) -> None: + super().__init__(e.status, e.message) + self.s = f"{e.status, e.message}. Failed to get token in init setup. Check your credentials." + logging.error(self.s) + + def __str__(self) -> str: + return self.s + + +class TokenRefreshException(AuthenticationException): + def __init__(self, e: AuthenticationException) -> None: + super().__init__(e.status, e.message) + self.s = f"{e.status, e.message}. Failed to update access token. Refresh token may be invalid or expired." + logging.error(self.s) + + def __str__(self) -> str: + return self.s diff --git a/Solutions/ESET Protect Platform/Data Connectors/integration/main.py b/Solutions/ESET Protect Platform/Data Connectors/integration/main.py new file mode 100644 index 00000000000..fb6547ad842 --- /dev/null +++ b/Solutions/ESET Protect Platform/Data Connectors/integration/main.py @@ -0,0 +1,105 @@ +import asyncio +import logging +import time +import typing as t +from datetime import datetime, timezone + +from integration.models import Config, EnvVariables, TokenStorage +from integration.utils import ( + LastDetectionTimeHandler, + RequestSender, + TokenProvider, + TransformerDetections, +) + + +class ServiceClient: + def __init__(self) -> None: + self.config = Config() + self.env_vars = EnvVariables() + self.last_detection_time_handler = LastDetectionTimeHandler( + self.env_vars.conn_str, + self.env_vars.last_detection_time, + ) + self.request_sender = RequestSender(self.config, self.env_vars) + self.token_provider = TokenProvider(TokenStorage(), self.request_sender, self.env_vars, self.config.buffer) + self.transformer_detections = TransformerDetections(self.env_vars) + self._is_running = False + self._next_page_token: str | None = None + self._cur_ld_time: str | None = None + + async def run(self) -> None: + if self._is_running: + while self._is_running: + await asyncio.gather(self._custom_sleep(), self._process_integration()) + else: + await asyncio.gather(self._process_integration()) + + async def _process_integration(self) -> None: + start_time = time.time() + max_duration = self.env_vars.interval * 60 + + while self._next_page_token != "" and (time.time() - start_time) < (max_duration - 30): + response_data = await self._call_service() + self._next_page_token = response_data.get("nextPageToken") if response_data else "" + + if response_data and response_data.get("detections") and (time.time() - start_time) < (max_duration - 15): + self._cur_ld_time, successful_data_upload = ( + await self.transformer_detections.send_integration_detections(response_data, self._cur_ld_time) + ) + self._next_page_token = "" if successful_data_upload is False else self._next_page_token + self._update_last_detection_time() + + def _update_last_detection_time(self) -> None: + if self._cur_ld_time and self._cur_ld_time != self.last_detection_time_handler.last_detection_time: + self.last_detection_time_handler.storage_table_handler.input_entity( + new_entity=self.last_detection_time_handler.get_entity_schema(self._cur_ld_time) # type: ignore[call-arg] + ) + + async def _custom_sleep(self) -> None: + logging.info(f"Start of the {self.env_vars.interval} seconds interval") + await asyncio.sleep(self.env_vars.interval) + logging.info(f"End of the {self.env_vars.interval} seconds interval") + + async def _call_service(self) -> dict[str, t.Any] | None: + logging.info(f"Service call initiated") + + if not self.token_provider.token.access_token or datetime.now(timezone.utc) > self.token_provider.token.expiration_time: # type: ignore + await self.token_provider.get_token() + + try: + if ( + self.token_provider.token.expiration_time + and datetime.now(timezone.utc) < self.token_provider.token.expiration_time + ): + data = await self.request_sender.send_request( + self.request_sender.send_request_get, + { + "Authorization": f"Bearer {self.token_provider.token.access_token}", + "Content-Type": "application/json", + }, + self.last_detection_time_handler.last_detection_time, + self._next_page_token, + ) + logging.info( + f"Service call response data is {'obtained' if data and data.get('detections') else f'empty: {data}'}" + ) + return data + + logging.info("Service not called due to missing token.") + except Exception as e: + logging.error(f"Error in running service call: {e}") + + return None + + +def main() -> None: + logging.basicConfig( + format="%(asctime)s - %(levelname)s - %(message)s", level=logging.INFO, datefmt="%Y-%m-%d %H:%M:%S" + ) + service_client = ServiceClient() + asyncio.run(service_client.run()) + + +if __name__ == "__main__": + main() diff --git a/Solutions/ESET Protect Platform/Data Connectors/integration/models.py b/Solutions/ESET Protect Platform/Data Connectors/integration/models.py new file mode 100644 index 00000000000..afbba7d4309 --- /dev/null +++ b/Solutions/ESET Protect Platform/Data Connectors/integration/models.py @@ -0,0 +1,101 @@ +import logging +import os +import typing as t +from dataclasses import dataclass, field +from datetime import datetime, timedelta, timezone +from importlib import resources + +import yaml + + +@dataclass +class TokenStorage: + __access_token: str | None = field(default=None, init=False) + __refresh_token: str | None = field(default=None, init=False) + __expiration_time: datetime | None = field(default=None, init=False) + + @property + def access_token(self) -> str | None: + return self.__access_token + + @access_token.setter + def access_token(self, value: str) -> None: + self.__access_token = value + + @property + def refresh_token(self) -> str | None: + return self.__refresh_token + + @refresh_token.setter + def refresh_token(self, value: str) -> None: + self.__refresh_token = value + + @property + def expiration_time(self) -> datetime | None: + return self.__expiration_time + + @expiration_time.setter + def expiration_time(self, value: datetime) -> None: + self.__expiration_time = value + + def to_dict(self) -> dict[str, t.Any]: + return { + "access_token": self.access_token, + "refresh_token": self.refresh_token, + "expiration_time": self.expiration_time, + } + + +class Config: + def __init__(self) -> None: + config = self.get_config_params() + if config: + self.max_retries: int = config.get("max_retries") # type: ignore + self.retry_delay: float = float(config.get("retry_delay")) # type: ignore + self.requests_timeout = config.get("requests_timeout") + self.buffer: int = config.get("buffer") # type: ignore + + def get_config_params(self) -> dict[str, t.Any] | t.Any: + try: + return yaml.safe_load( + resources.files(__package__ or "integration").parent.joinpath("config.yml").read_bytes() + ) + except FileNotFoundError as e: + logging.error(e) + raise FileNotFoundError("The config file is not found. Further processing is impossible.") + + +class EnvVariables: + def __init__(self) -> None: + self.__username: str | None = os.getenv("USERNAME_INTEGRATION") + self.__password: str | None = os.getenv("PASSWORD_INTEGRATION") + self.interval: int = int(os.getenv("INTERVAL", 5)) + self.last_detection_time: str = os.getenv( + "LAST_DETECTION", + (datetime.now(timezone.utc) - timedelta(seconds=self.interval * 60)).strftime("%Y-%m-%dT%H:%M:%SZ"), + ) + self.endpoint_uri: str = os.getenv("ENDPOINT_URI", "") + self.dcr_immutableid: str = os.getenv("DCR_IMMUTABLEID", "") + self.stream_name: str = os.getenv("STREAM_NAME", "") + self.__conn_str: str = os.getenv("WEBSITE_CONTENTAZUREFILECONNECTIONSTRING", "") + self.__key_base64: str = os.getenv("KEY_BASE64", "") + + region = os.getenv("INSTANCE_REGION", "") + self.oauth_url: str = f"https://{region}.business-account.iam.eset.systems" + self.detections_url: str = f"https://{region}.incident-management.eset.systems/v1/detections" + + @property + def username(self) -> str | None: + return self.__username + + @property + def password(self) -> str | None: + return self.__password + + @property + def conn_str(self) -> str: + return self.__conn_str + + @property + def key_base64(self) -> str: + return self.__key_base64 diff --git a/Solutions/ESET Protect Platform/Data Connectors/integration/models_detections.py b/Solutions/ESET Protect Platform/Data Connectors/integration/models_detections.py new file mode 100644 index 00000000000..2684ba196de --- /dev/null +++ b/Solutions/ESET Protect Platform/Data Connectors/integration/models_detections.py @@ -0,0 +1,49 @@ +from datetime import datetime + +from pydantic import BaseModel, Field + + +class NetworkCommunication(BaseModel): + direction: str + localIpAddress: str + localPort: int + protocolName: str + remoteIpAddress: str + remotePort: int + + +class Context(BaseModel): + circumstances: str + deviceUuid: str + process: dict[str, str] + userName: str + + +class Response(BaseModel): + description: str + deviceRestartRequired: bool + displayName: str + protectionName: str + + +class Detection(BaseModel): + context: Context + networkCommunication: NetworkCommunication + responses: list[Response] + category: str + displayName: str + objectHashSha1: str + objectName: str + objectTypeName: str + objectUrl: str + occurTime: str + TimeGenerated: str = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ") + severityLevel: str + typeName: str + customUuid: str = Field(alias="uuid") + + +class Detections(BaseModel): + detections: list[Detection] + nextPageToken: str + totalSize: int diff --git a/Solutions/ESET Protect Platform/Data Connectors/integration/py.typed b/Solutions/ESET Protect Platform/Data Connectors/integration/py.typed new file mode 100644 index 00000000000..e69de29bb2d diff --git a/Solutions/ESET Protect Platform/Data Connectors/integration/utils.py b/Solutions/ESET Protect Platform/Data Connectors/integration/utils.py new file mode 100644 index 00000000000..485561765a1 --- /dev/null +++ b/Solutions/ESET Protect Platform/Data Connectors/integration/utils.py @@ -0,0 +1,289 @@ +import asyncio +import logging +import typing as t +import urllib.parse +from datetime import datetime, timedelta, timezone + +import aiohttp +from aiohttp.client_exceptions import ClientResponseError +from azure.core.exceptions import HttpResponseError, ServiceRequestError +from azure.data.tables import TableServiceClient +from azure.identity.aio import DefaultAzureCredential +from azure.monitor.ingestion.aio import LogsIngestionClient +from cryptography.fernet import Fernet, InvalidToken +from integration.exceptions import ( + AuthenticationException, + InvalidCredentialsException, + MissingCredentialsException, + TokenRefreshException, +) +from integration.models import Config, EnvVariables, TokenStorage +from integration.models_detections import Detection, Detections +from pydantic import ValidationError + + +class RequestSender: + def __init__(self, config: Config, env_vars: EnvVariables): + self.config = config + self.env_vars = env_vars + + async def send_request( + self, + send_request_fun: ( + t.Callable[ + [aiohttp.client.ClientSession, str, str | None], t.Coroutine[t.Any, t.Any, dict[str, str | int] | t.Any] + ] + | t.Callable[ + [aiohttp.client.ClientSession, str | None], t.Coroutine[t.Any, t.Any, dict[str, str | int] | t.Any] + ] + ), + headers: dict[str, t.Any] | None = None, + *data: t.Any, + ) -> t.Dict[str, str | int] | None: + retries = 0 + + while retries < self.config.max_retries: + try: + async with aiohttp.ClientSession(headers=headers, raise_for_status=True) as session: + return await send_request_fun(session, *data) + + except ClientResponseError as e: + if e.status in [400, 401, 403]: + raise AuthenticationException(status=e.status, message=e.message) + + retries += 1 + logging.error( + f"Exception: {e.status} {e.message}. Request failed. " + f"Request retry attempt: {retries}/{self.config.max_retries}" + ) + await asyncio.sleep(self.config.retry_delay) + return None + + async def send_request_post( + self, session: aiohttp.client.ClientSession, grant_type: str | None + ) -> t.Dict[str, str | int] | t.Any: + logging.info("Sending token request") + + async with session.post( + url=f"{self.env_vars.oauth_url}/oauth/token", + data=urllib.parse.quote(f"grant_type={grant_type}", safe="=&/"), + timeout=self.config.requests_timeout, + ) as response: + return await response.json() + + async def send_request_get( + self, session: aiohttp.client.ClientSession, last_detection_time: str, next_page_token: str | None + ) -> t.Dict[str, str | int] | t.Any: + logging.info("Sending service request") + + async with session.get( + self.env_vars.detections_url, params=self._prepare_get_request_params(last_detection_time, next_page_token) + ) as response: + return await response.json() + + def _prepare_get_request_params(self, last_detection_time: str, next_page_token: str | None) -> dict[str, t.Any]: + params = {"pageSize": 100} + if next_page_token not in ["", None]: + params["pageToken"] = next_page_token # type: ignore[assignment] + if last_detection_time: + params["startTime"] = last_detection_time # type: ignore[assignment] + + return params + + +class TokenProvider: + def __init__(self, token: TokenStorage, requests_sender: RequestSender, env_vars: EnvVariables, buffer: int): + self.token = token + self.requests_sender = requests_sender + self.env_vars = env_vars + self.buffer = buffer + self.fernet = Fernet(self.env_vars.key_base64.encode("utf-8")) + self.storage_table_name = "TokenParams" + self.storage_table_handler = StorageTableHandler(self.env_vars.conn_str, self.storage_table_name) + self.storage_table_handler.set_entity() + + self.get_token_params_from_storage() + + def get_token_params_from_storage(self) -> None: + if not self.storage_table_handler.entities: + return None + for token_param in self.token.to_dict().keys(): + value = self.storage_table_handler.entities.get(token_param) + if isinstance(value, bytes): + try: + value = self.fernet.decrypt(value).decode("utf-8") + except InvalidToken: + logging.warning("Issue with decrypt: Invalid Token") + value = "" + setattr(self.token, token_param, value) + + async def get_token(self) -> None: + logging.info("Getting token") + + if not self.token.access_token and (not self.env_vars.username or not self.env_vars.password): + raise MissingCredentialsException() + + grant_type = ( + f"refresh_token&refresh_token={self.token.refresh_token}" + if self.token.access_token + else f"password&username={self.env_vars.username}&password={self.env_vars.password}" + ) + + try: + response = await self.requests_sender.send_request( + self.requests_sender.send_request_post, + {"Content-type": "application/x-www-form-urlencoded"}, + grant_type, + ) + except AuthenticationException as e: + if not self.token.access_token: + raise InvalidCredentialsException(e) + else: + self.storage_table_handler.input_entity({k: "" for k in self.token.to_dict()}) # type: ignore[call-arg] + raise TokenRefreshException(e) + + if response: + self.set_token_params_locally_and_in_storage(response) + logging.info("Token obtained successfully") + + def set_token_params_locally_and_in_storage(self, response: t.Dict[str, str | int]) -> None: + self.token.access_token = t.cast(str, response["access_token"]) + self.token.refresh_token = t.cast(str, response["refresh_token"]) + self.token.expiration_time = datetime.now(timezone.utc) + timedelta( + seconds=int(response["expires_in"]) - self.buffer + ) + self.storage_table_handler.input_entity( + { + k: self.fernet.encrypt(v.encode("utf-8")) if type(v) is str else v + for k, v in self.token.to_dict().items() + } + ) # type: ignore[call-arg] + + +class TransformerDetections: + def __init__(self, env_vars: EnvVariables) -> None: + self.env_vars = env_vars + + async def send_integration_detections( + self, detections: dict[str, t.Any] | None, last_detection: str | None + ) -> tuple[str | None, bool]: + validated_detections = self._validate_detections_data(detections) + if not validated_detections: + return last_detection, False + return await self._send_data_to_log_analytics_workspace(validated_detections, last_detection) + + def _validate_detections_data(self, response_data: dict[str, t.Any] | None) -> list[Detection] | None: + if not response_data: + logging.info("No new detections") + return None + try: + return Detections.model_validate(response_data).detections + except ValidationError as e: + logging.error(e) + validated_detections = [] + for detection in response_data.get("detections"): # type: ignore + try: + validated_detections.append(Detection.model_validate(detection)) + except ValidationError as e: + logging.error(e) + + return validated_detections + + async def _send_data_to_log_analytics_workspace( + self, validated_data: t.List[Detection], last_detection: str | None, successful_data_upload: bool = False + ) -> tuple[str | None, bool]: + credential = DefaultAzureCredential() # Env vars: AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_TENANT_ID + client = LogsIngestionClient(endpoint=self.env_vars.endpoint_uri, credential=credential, logging_enable=True) + + async with client: + try: + self._update_time_generated(validated_data) + dumped_data = [d.model_dump() for d in validated_data] + + await client.upload( + rule_id=self.env_vars.dcr_immutableid, + stream_name=self.env_vars.stream_name, + logs=dumped_data, # type: ignore[arg-type] + ) + last_detection = max(validated_data, key=lambda detection: detection.occurTime).occurTime + successful_data_upload = True + except ServiceRequestError as e: + logging.error(f"Authentication to Azure service failed: {e}") + except HttpResponseError as e: + logging.error(f"Upload failed: {e}") + + await credential.close() + return last_detection, successful_data_upload + + def _update_time_generated(self, validated_data: t.List[Detection]) -> None: + utc_now = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ") + for data in validated_data: + data.TimeGenerated = utc_now + + +class StorageTableHandler: + def __init__(self, env_conn_str: str, table_name_keys: str) -> None: + self.conn_str = env_conn_str + self.table_name_keys = table_name_keys + self.entities = None + self.table_client = None + + def with_table_client(func: t.Callable[[t.Any, t.Any], t.Any]) -> t.Callable[[t.Any], t.Any]: # type: ignore + def wrapper(storage_table_handler_instance, *args, **kwargs): # type: ignore[no-untyped-def] + try: + with TableServiceClient.from_connection_string( + conn_str=storage_table_handler_instance.conn_str + ) as table_service_client: + storage_table_handler_instance.table_client = table_service_client.create_table_if_not_exists( + table_name=storage_table_handler_instance.table_name_keys + ) + return func(storage_table_handler_instance, *args, **kwargs) + except ValueError as e: + raise ValueError(f"Connection string WEBSITE_CONTENTAZUREFILECONNECTIONSTRING value error: {e}") + + return wrapper + + @with_table_client # type: ignore + def set_entity(self) -> None: + if self.table_client: + self.entities = next(self.table_client.query_entities(""), None) + return None + + @with_table_client + def input_entity(self, new_entity: dict[str, t.Any]) -> None: + entity = { + "PartitionKey": self.table_name_keys, + "RowKey": self.table_name_keys, + "TimeGenerated": datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"), + } | new_entity + try: + if self.table_client: + ( + self.table_client.update_entity(entity=entity) + if self.entities + else self.table_client.create_entity(entity=entity) + ) + logging.info(f"Entity: {self.table_name_keys} updated") + except Exception as e: + print("Exception occurred:", e) + + +class LastDetectionTimeHandler: + def __init__(self, storage_table_conn_str: str, env_last_occur_time: str) -> None: + self.storage_table_name = "LastDetectionTime" + self.storage_table_handler = StorageTableHandler(storage_table_conn_str, self.storage_table_name) + self.storage_table_handler.set_entity() + self.last_detection_time = self.get_last_occur_time(env_last_occur_time) + + def get_last_occur_time(self, env_last_occur_time: str) -> t.Any: + if self.storage_table_handler.entities: + return self.storage_table_handler.entities.get(self.storage_table_name) + return env_last_occur_time + + def get_entity_schema(self, cur_last_detection_time: str) -> dict[str, t.Any]: + return { + self.storage_table_name: ( + datetime.strptime(cur_last_detection_time, "%Y-%m-%dT%H:%M:%SZ") + timedelta(seconds=1) + ).isoformat() + + "Z" + } diff --git a/Solutions/ESET Protect Platform/Data Connectors/requirements.txt b/Solutions/ESET Protect Platform/Data Connectors/requirements.txt new file mode 100644 index 00000000000..ade7a02d953 --- /dev/null +++ b/Solutions/ESET Protect Platform/Data Connectors/requirements.txt @@ -0,0 +1,30 @@ +aiohttp==3.9.5 ; python_version >= "3.11" and python_version < "4.0" +aiosignal==1.3.1 ; python_version >= "3.11" and python_version < "4.0" +annotated-types==0.7.0 ; python_version >= "3.11" and python_version < "4.0" +attrs==24.2.0 ; python_version >= "3.11" and python_version < "4.0" +azure-core==1.30.2 ; python_version >= "3.11" and python_version < "4.0" +azure-data-tables==12.5.0 ; python_version >= "3.11" and python_version < "4.0" +azure-functions==1.20.0 ; python_version >= "3.11" and python_version < "4.0" +azure-identity==1.17.1 ; python_version >= "3.11" and python_version < "4.0" +azure-monitor-ingestion==1.0.4 ; python_version >= "3.11" and python_version < "4.0" +certifi==2024.8.30 ; python_version >= "3.11" and python_version < "4.0" +cffi==1.17.1 ; python_version >= "3.11" and python_version < "4.0" and platform_python_implementation != "PyPy" +charset-normalizer==3.3.2 ; python_version >= "3.11" and python_version < "4.0" +cryptography==43.0.1 ; python_version >= "3.11" and python_version < "4.0" +frozenlist==1.4.1 ; python_version >= "3.11" and python_version < "4.0" +idna==3.10 ; python_version >= "3.11" and python_version < "4.0" +isodate==0.6.1 ; python_version >= "3.11" and python_version < "4.0" +msal-extensions==1.2.0 ; python_version >= "3.11" and python_version < "4.0" +msal==1.31.0 ; python_version >= "3.11" and python_version < "4.0" +multidict==6.1.0 ; python_version >= "3.11" and python_version < "4.0" +portalocker==2.10.1 ; python_version >= "3.11" and python_version < "4.0" +pycparser==2.22 ; python_version >= "3.11" and python_version < "4.0" and platform_python_implementation != "PyPy" +pydantic-core==2.20.1 ; python_version >= "3.11" and python_version < "4.0" +pydantic==2.8.2 ; python_version >= "3.11" and python_version < "4.0" +pyjwt[crypto]==2.9.0 ; python_version >= "3.11" and python_version < "4.0" +pyyaml==6.0.1 ; python_version >= "3.11" and python_version < "4.0" +requests==2.32.3 ; python_version >= "3.11" and python_version < "4.0" +six==1.16.0 ; python_version >= "3.11" and python_version < "4.0" +typing-extensions==4.12.2 ; python_version >= "3.11" and python_version < "4.0" +urllib3==2.2.3 ; python_version >= "3.11" and python_version < "4.0" +yarl==1.13.1 ; python_version >= "3.11" and python_version < "4.0" diff --git a/Solutions/ESET Protect Platform/Data/Solution_ESETProtectPlatform.json b/Solutions/ESET Protect Platform/Data/Solution_ESETProtectPlatform.json new file mode 100644 index 00000000000..b6813c11bcf --- /dev/null +++ b/Solutions/ESET Protect Platform/Data/Solution_ESETProtectPlatform.json @@ -0,0 +1,14 @@ +{ + "Name": "ESET Protect Platform", + "Author": "ESET", + "Logo": "", + "Description": "ESET Protect Platform solution for Microsoft Sentinel ingests detections from [ESET Protect Platform](https://www.eset.com/int/business/protect-platform/) using the provided [Integration REST API](https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/ESET%20Protect%20Platform/Data%20Connectors). \n\n**Underlying Microsoft Technologies used:**\n\nThe ESET Protect Platform solution takes a dependency on the following technologies, and some of these dependencies either may be in [Preview](https://azure.microsoft.com/support/legal/preview-supplemental-terms/) state or might result in additional ingestion or operational costs:\n\na. [Logs Ingestion API in Azure Monitor](https://learn.microsoft.com/en-us/azure/azure-monitor/logs/logs-ingestion-api-overview)\n\nb. [Azure Functions](https://azure.microsoft.com/services/functions/#overview)\n\n", + "Data Connectors": [ + "Data Connectors/ESETProtectPlatform_API_FunctionApp.json" + ], + "BasePath": "C:\\GitHub\\Azure-Sentinel\\Solutions\\ESET Protect Platform", + "Version": "1.0.0", + "Metadata": "SolutionMetadata.json", + "TemplateSpec": true, + "Is1Pconnector": false + } \ No newline at end of file diff --git a/Solutions/ESET Protect Platform/Package/3.0.0.zip b/Solutions/ESET Protect Platform/Package/3.0.0.zip new file mode 100644 index 00000000000..e998ac2e8b9 Binary files /dev/null and b/Solutions/ESET Protect Platform/Package/3.0.0.zip differ diff --git a/Solutions/ESET Protect Platform/Package/createUiDefinition.json b/Solutions/ESET Protect Platform/Package/createUiDefinition.json new file mode 100644 index 00000000000..11bb249e26d --- /dev/null +++ b/Solutions/ESET Protect Platform/Package/createUiDefinition.json @@ -0,0 +1,85 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/0.1.2-preview/CreateUIDefinition.MultiVm.json#", + "handler": "Microsoft.Azure.CreateUIDef", + "version": "0.1.2-preview", + "parameters": { + "config": { + "isWizard": false, + "basics": { + "description": "\n\n**Note:** Please refer to the following before installing the solution: \n\n• Review the solution [Release Notes](https://github.com/Azure/Azure-Sentinel/tree/master/Solutions/ESET%20Protect%20Platform/ReleaseNotes.md)\n\n • There may be [known issues](https://aka.ms/sentinelsolutionsknownissues) pertaining to this Solution, please refer to them before installing.\n\nESET Protect Platform solution for Microsoft Sentinel ingests detections from [ESET Protect Platform](https://www.eset.com/int/business/protect-platform/) using the provided [Integration REST API](https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/ESET%20Protect%20Platform/Data%20Connectors). \n\n**Underlying Microsoft Technologies used:**\n\nThe ESET Protect Platform solution takes a dependency on the following technologies, and some of these dependencies either may be in [Preview](https://azure.microsoft.com/support/legal/preview-supplemental-terms/) state or might result in additional ingestion or operational costs:\n\na. [Logs Ingestion API in Azure Monitor](https://learn.microsoft.com/en-us/azure/azure-monitor/logs/logs-ingestion-api-overview)\n\nb. [Azure Functions](https://azure.microsoft.com/services/functions/#overview)\n\n\n\n**Data Connectors:** 1\n\n[Learn more about Microsoft Sentinel](https://aka.ms/azuresentinel) | [Learn more about Solutions](https://aka.ms/azuresentinelsolutionsdoc)", + "subscription": { + "resourceProviders": [ + "Microsoft.OperationsManagement/solutions", + "Microsoft.OperationalInsights/workspaces/providers/alertRules", + "Microsoft.Insights/workbooks", + "Microsoft.Logic/workflows" + ] + }, + "location": { + "metadata": { + "hidden": "Hiding location, we get it from the log analytics workspace" + }, + "visible": false + }, + "resourceGroup": { + "allowExisting": true + } + } + }, + "basics": [ + { + "name": "getLAWorkspace", + "type": "Microsoft.Solutions.ArmApiControl", + "toolTip": "This filters by workspaces that exist in the Resource Group selected", + "condition": "[greater(length(resourceGroup().name),0)]", + "request": { + "method": "GET", + "path": "[concat(subscription().id,'/providers/Microsoft.OperationalInsights/workspaces?api-version=2020-08-01')]" + } + }, + { + "name": "workspace", + "type": "Microsoft.Common.DropDown", + "label": "Workspace", + "placeholder": "Select a workspace", + "toolTip": "This dropdown will list only workspace that exists in the Resource Group selected", + "constraints": { + "allowedValues": "[map(filter(basics('getLAWorkspace').value, (filter) => contains(toLower(filter.id), toLower(resourceGroup().name))), (item) => parse(concat('{\"label\":\"', item.name, '\",\"value\":\"', item.name, '\"}')))]", + "required": true + }, + "visible": true + } + ], + "steps": [ + { + "name": "dataconnectors", + "label": "Data Connectors", + "bladeTitle": "Data Connectors", + "elements": [ + { + "name": "dataconnectors1-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "This Solution installs the data connector for ESET Protect Platform. You can get ESET Protect Platform custom log data in your Microsoft Sentinel workspace. After installing the solution, configure and enable this data connector by following guidance in Manage solution view." + } + }, + { + "name": "dataconnectors-link2", + "type": "Microsoft.Common.TextBlock", + "options": { + "link": { + "label": "Learn more about connecting data sources", + "uri": "https://docs.microsoft.com/azure/sentinel/connect-data-sources" + } + } + } + ] + } + ], + "outputs": { + "workspace-location": "[first(map(filter(basics('getLAWorkspace').value, (filter) => and(contains(toLower(filter.id), toLower(resourceGroup().name)),equals(filter.name,basics('workspace')))), (item) => item.location))]", + "location": "[location()]", + "workspace": "[basics('workspace')]" + } + } +} diff --git a/Solutions/ESET Protect Platform/Package/mainTemplate.json b/Solutions/ESET Protect Platform/Package/mainTemplate.json new file mode 100644 index 00000000000..75f6456cd17 --- /dev/null +++ b/Solutions/ESET Protect Platform/Package/mainTemplate.json @@ -0,0 +1,393 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "author": "ESET", + "comments": "Solution template for ESET Protect Platform" + }, + "parameters": { + "location": { + "type": "string", + "minLength": 1, + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Not used, but needed to pass arm-ttk test `Location-Should-Not-Be-Hardcoded`. We instead use the `workspace-location` which is derived from the LA workspace" + } + }, + "workspace-location": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "[concat('Region to deploy solution resources -- separate from location selection',parameters('location'))]" + } + }, + "workspace": { + "defaultValue": "", + "type": "string", + "metadata": { + "description": "Workspace name for Log Analytics where Microsoft Sentinel is setup" + } + } + }, + "variables": { + "_solutionName": "ESET Protect Platform", + "_solutionVersion": "3.0.0", + "solutionId": "eset.eset-protect-platform-solution", + "_solutionId": "[variables('solutionId')]", + "uiConfigId1": "ESETProtectPlatform", + "_uiConfigId1": "[variables('uiConfigId1')]", + "dataConnectorContentId1": "ESETProtectPlatform", + "_dataConnectorContentId1": "[variables('dataConnectorContentId1')]", + "dataConnectorId1": "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/dataConnectors', variables('_dataConnectorContentId1'))]", + "_dataConnectorId1": "[variables('dataConnectorId1')]", + "dataConnectorTemplateSpecName1": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-dc-',uniquestring(variables('_dataConnectorContentId1'))))]", + "dataConnectorVersion1": "1.0.0", + "_dataConnectorcontentProductId1": "[concat(take(variables('_solutionId'),50),'-','dc','-', uniqueString(concat(variables('_solutionId'),'-','DataConnector','-',variables('_dataConnectorContentId1'),'-', variables('dataConnectorVersion1'))))]", + "_solutioncontentProductId": "[concat(take(variables('_solutionId'),50),'-','sl','-', uniqueString(concat(variables('_solutionId'),'-','Solution','-',variables('_solutionId'),'-', variables('_solutionVersion'))))]" + }, + "resources": [ + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[variables('dataConnectorTemplateSpecName1')]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "description": "ESET Protect Platform data connector with template version 3.0.0", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('dataConnectorVersion1')]", + "parameters": {}, + "variables": {}, + "resources": [ + { + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',variables('_dataConnectorContentId1'))]", + "apiVersion": "2021-03-01-preview", + "type": "Microsoft.OperationalInsights/workspaces/providers/dataConnectors", + "location": "[parameters('workspace-location')]", + "kind": "GenericUI", + "properties": { + "connectorUiConfig": { + "id": "[variables('_uiConfigId1')]", + "title": "ESET Protect Platform (using Azure Functions)", + "publisher": "ESET", + "descriptionMarkdown": "The ESET Protect Platform data connector enables users to inject detections data from [ESET Protect Platform](https://www.eset.com/int/business/protect-platform/) using the provided [Integration REST API](https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/ESET%20Protect%20Platform/Data%20Connectors). Integration REST API runs as scheduled Azure Function App.", + "graphQueries": [ + { + "metricName": "Total data received", + "legend": "IntegrationTable_CL", + "baseQuery": "IntegrationTable_CL" + } + ], + "sampleQueries": [ + { + "description": "All table records sorted by time", + "query": "IntegrationTable_CL\n| sort by TimeGenerated desc" + } + ], + "dataTypes": [ + { + "name": "IntegrationTable_CL", + "lastDataReceivedQuery": "IntegrationTable_CL\n | summarize Time = max(TimeGenerated)\n | where isnotempty(Time)" + } + ], + "connectivityCriterias": [ + { + "type": "IsConnectedQuery", + "value": [ + "IntegrationTable_CL\n | summarize LastLogReceived = max(TimeGenerated)\n | project IsConnected = LastLogReceived > ago(30d)" + ] + } + ], + "availability": { + "status": 1, + "isPreview": false + }, + "permissions": { + "resourceProvider": [ + { + "provider": "Microsoft.OperationalInsights/workspaces", + "permissionsDisplayText": "read and write permissions on the workspace are required.", + "providerDisplayName": "Workspace", + "scope": "Workspace", + "requiredPermissions": { + "write": true, + "read": true, + "delete": true + } + }, + { + "provider": "Microsoft.OperationalInsights/workspaces/sharedKeys", + "permissionsDisplayText": "read permissions to shared keys for the workspace are required. [See the documentation to learn more about workspace keys](https://docs.microsoft.com/azure/azure-monitor/platform/agent-windows#obtain-workspace-id-and-key).", + "providerDisplayName": "Keys", + "scope": "Workspace", + "requiredPermissions": { + "action": true + } + } + ], + "customs": [ + { + "name": "Microsoft.Web/sites permissions", + "description": "Read and write permissions to Azure Functions to create a Function App is required. [See the documentation to learn more about Azure Functions](https://docs.microsoft.com/azure/azure-functions/)." + }, + { + "name": "Permission to register an application in Microsoft Entra ID", + "description": "Sufficient permissions to register an application with your Microsoft Entra tenant are required." + }, + { + "name": "Permission to assign a role to the registered application", + "description": "Permission to assign the Monitoring Metrics Publisher role to the registered application in Microsoft Entra ID is required." + } + ] + }, + "instructionSteps": [ + { + "description": ">**NOTE:** The ESET Protect Platform data connector uses Azure Functions to connect to the ESET Protect Platform via Eset Connect API to pull detections logs into Microsoft Sentinel. This process might result in additional data ingestion costs. See details on the [Azure Functions pricing page](https://azure.microsoft.com/pricing/details/functions/)." + }, + { + "description": "Use this [instruction](https://help.eset.com/eset_connect/en-US/create_api_user_account.html) to create an ESET Connect API User account with **Login** and **Password**.", + "title": "Step 1 - Create an API user" + }, + { + "description": "Create a Microsoft Entra ID registered application by following the steps in the [Register a new application instruction.](https://learn.microsoft.com/en-us/azure/healthcare-apis/register-application#register-a-new-application)", + "title": "Step 2 - Create a registered application" + }, + { + "description": "\n\n1. Click the **Deploy to Azure** button below. \n\n\t[![Deploy To Azure](https://aka.ms/deploytoazurebutton)](https://aka.ms/sentinel-EsetProtectionPlatform-azuredeploy)\n\n2. Select the name of the **Log Analytics workspace** associated with your Microsoft Sentinel. Select the same **Resource Group** as the Resource Group of the Log Analytics workspace.\n\n3. Type the parameters of the registered application in Microsoft Entra ID: **Azure Client ID**, **Azure Client Secret**, **Azure Tenant ID**, **Object ID**. You can find the **Object ID** on Azure Portal by following this path \n> Microsoft Entra ID -> Manage (on the left-side menu) -> Enterprise applications -> Object ID column (the value next to your registered application name).\n\n4. Provide the ESET Connect API user account **Login** and **Password** obtained in **Step 1**.", + "title": "Step 3 - Deploy the ESET Protect Platform data connector using the Azure Resource Manager (ARM) template" + } + ] + } + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2023-04-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('DataConnector-', last(split(variables('_dataConnectorId1'),'/'))))]", + "properties": { + "parentId": "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/dataConnectors', variables('_dataConnectorContentId1'))]", + "contentId": "[variables('_dataConnectorContentId1')]", + "kind": "DataConnector", + "version": "[variables('dataConnectorVersion1')]", + "source": { + "kind": "Solution", + "name": "ESET Protect Platform", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "ESET" + }, + "support": { + "name": "ESET Enterprise Integrations", + "email": "eset-enterpise-integration@eset.com", + "tier": "Partner", + "link": "https://help.eset.com/eset_connect/en-US/integrations.html" + } + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('_dataConnectorContentId1')]", + "contentKind": "DataConnector", + "displayName": "ESET Protect Platform (using Azure Functions)", + "contentProductId": "[variables('_dataConnectorcontentProductId1')]", + "id": "[variables('_dataConnectorcontentProductId1')]", + "version": "[variables('dataConnectorVersion1')]" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2023-04-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('DataConnector-', last(split(variables('_dataConnectorId1'),'/'))))]", + "dependsOn": [ + "[variables('_dataConnectorId1')]" + ], + "location": "[parameters('workspace-location')]", + "properties": { + "parentId": "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/dataConnectors', variables('_dataConnectorContentId1'))]", + "contentId": "[variables('_dataConnectorContentId1')]", + "kind": "DataConnector", + "version": "[variables('dataConnectorVersion1')]", + "source": { + "kind": "Solution", + "name": "ESET Protect Platform", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "ESET" + }, + "support": { + "name": "ESET Enterprise Integrations", + "email": "eset-enterpise-integration@eset.com", + "tier": "Partner", + "link": "https://help.eset.com/eset_connect/en-US/integrations.html" + } + } + }, + { + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',variables('_dataConnectorContentId1'))]", + "apiVersion": "2021-03-01-preview", + "type": "Microsoft.OperationalInsights/workspaces/providers/dataConnectors", + "location": "[parameters('workspace-location')]", + "kind": "GenericUI", + "properties": { + "connectorUiConfig": { + "title": "ESET Protect Platform (using Azure Functions)", + "publisher": "ESET", + "descriptionMarkdown": "The ESET Protect Platform data connector enables users to inject detections data from [ESET Protect Platform](https://www.eset.com/int/business/protect-platform/) using the provided [Integration REST API](https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/ESET%20Protect%20Platform/Data%20Connectors). Integration REST API runs as scheduled Azure Function App.", + "graphQueries": [ + { + "metricName": "Total data received", + "legend": "IntegrationTable_CL", + "baseQuery": "IntegrationTable_CL" + } + ], + "dataTypes": [ + { + "name": "IntegrationTable_CL", + "lastDataReceivedQuery": "IntegrationTable_CL\n | summarize Time = max(TimeGenerated)\n | where isnotempty(Time)" + } + ], + "connectivityCriterias": [ + { + "type": "IsConnectedQuery", + "value": [ + "IntegrationTable_CL\n | summarize LastLogReceived = max(TimeGenerated)\n | project IsConnected = LastLogReceived > ago(30d)" + ] + } + ], + "sampleQueries": [ + { + "description": "All table records sorted by time", + "query": "IntegrationTable_CL\n| sort by TimeGenerated desc" + } + ], + "availability": { + "status": 1, + "isPreview": false + }, + "permissions": { + "resourceProvider": [ + { + "provider": "Microsoft.OperationalInsights/workspaces", + "permissionsDisplayText": "read and write permissions on the workspace are required.", + "providerDisplayName": "Workspace", + "scope": "Workspace", + "requiredPermissions": { + "write": true, + "read": true, + "delete": true + } + }, + { + "provider": "Microsoft.OperationalInsights/workspaces/sharedKeys", + "permissionsDisplayText": "read permissions to shared keys for the workspace are required. [See the documentation to learn more about workspace keys](https://docs.microsoft.com/azure/azure-monitor/platform/agent-windows#obtain-workspace-id-and-key).", + "providerDisplayName": "Keys", + "scope": "Workspace", + "requiredPermissions": { + "action": true + } + } + ], + "customs": [ + { + "name": "Microsoft.Web/sites permissions", + "description": "Read and write permissions to Azure Functions to create a Function App is required. [See the documentation to learn more about Azure Functions](https://docs.microsoft.com/azure/azure-functions/)." + }, + { + "name": "Permission to register an application in Microsoft Entra ID", + "description": "Sufficient permissions to register an application with your Microsoft Entra tenant are required." + }, + { + "name": "Permission to assign a role to the registered application", + "description": "Permission to assign the Monitoring Metrics Publisher role to the registered application in Microsoft Entra ID is required." + } + ] + }, + "instructionSteps": [ + { + "description": ">**NOTE:** The ESET Protect Platform data connector uses Azure Functions to connect to the ESET Protect Platform via Eset Connect API to pull detections logs into Microsoft Sentinel. This process might result in additional data ingestion costs. See details on the [Azure Functions pricing page](https://azure.microsoft.com/pricing/details/functions/)." + }, + { + "description": "Use this [instruction](https://help.eset.com/eset_connect/en-US/create_api_user_account.html) to create an ESET Connect API User account with **Login** and **Password**.", + "title": "Step 1 - Create an API user" + }, + { + "description": "Create a Microsoft Entra ID registered application by following the steps in the [Register a new application instruction.](https://learn.microsoft.com/en-us/azure/healthcare-apis/register-application#register-a-new-application)", + "title": "Step 2 - Create a registered application" + }, + { + "description": "\n\n1. Click the **Deploy to Azure** button below. \n\n\t[![Deploy To Azure](https://aka.ms/deploytoazurebutton)](https://aka.ms/sentinel-EsetProtectionPlatform-azuredeploy)\n\n2. Select the name of the **Log Analytics workspace** associated with your Microsoft Sentinel. Select the same **Resource Group** as the Resource Group of the Log Analytics workspace.\n\n3. Type the parameters of the registered application in Microsoft Entra ID: **Azure Client ID**, **Azure Client Secret**, **Azure Tenant ID**, **Object ID**. You can find the **Object ID** on Azure Portal by following this path \n> Microsoft Entra ID -> Manage (on the left-side menu) -> Enterprise applications -> Object ID column (the value next to your registered application name).\n\n4. Provide the ESET Connect API user account **Login** and **Password** obtained in **Step 1**.", + "title": "Step 3 - Deploy the ESET Protect Platform data connector using the Azure Resource Manager (ARM) template" + } + ], + "id": "[variables('_uiConfigId1')]" + } + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentPackages", + "apiVersion": "2023-04-01-preview", + "location": "[parameters('workspace-location')]", + "properties": { + "version": "3.0.0", + "kind": "Solution", + "contentSchemaVersion": "3.0.0", + "displayName": "ESET Protect Platform", + "publisherDisplayName": "ESET Enterprise Integrations", + "descriptionHtml": "
Note: Please refer to the following before installing the solution:
\n• Review the solution Release Notes
\n• There may be known issues pertaining to this Solution, please refer to them before installing.
\nESET Protect Platform solution for Microsoft Sentinel ingests detections from ESET Protect Platform using the provided Integration REST API.
\nUnderlying Microsoft Technologies used:
\nThe ESET Protect Platform solution takes a dependency on the following technologies, and some of these dependencies either may be in Preview state or might result in additional ingestion or operational costs:
\n\nData Connectors: 1
\nLearn more about Microsoft Sentinel | Learn more about Solutions
\n", + "contentKind": "Solution", + "contentProductId": "[variables('_solutioncontentProductId')]", + "id": "[variables('_solutioncontentProductId')]", + "icon": "", + "contentId": "[variables('_solutionId')]", + "parentId": "[variables('_solutionId')]", + "source": { + "kind": "Solution", + "name": "ESET Protect Platform", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "ESET" + }, + "support": { + "name": "ESET Enterprise Integrations", + "email": "eset-enterpise-integration@eset.com", + "tier": "Partner", + "link": "https://help.eset.com/eset_connect/en-US/integrations.html" + }, + "dependencies": { + "operator": "AND", + "criteria": [ + { + "kind": "DataConnector", + "contentId": "[variables('_dataConnectorContentId1')]", + "version": "[variables('dataConnectorVersion1')]" + } + ] + }, + "firstPublishDate": "2024-10-15", + "lastPublishDate": "2024-10-15", + "providers": [ + "ESET Enterprise Integrations" + ], + "categories": { + "domains": [ + "Security - Automation (SOAR)", + "Security - Threat Protection" + ] + } + }, + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/', variables('_solutionId'))]" + } + ], + "outputs": {} +} diff --git a/Solutions/ESET Protect Platform/Package/testParameters.json b/Solutions/ESET Protect Platform/Package/testParameters.json new file mode 100644 index 00000000000..e55ec41a9ac --- /dev/null +++ b/Solutions/ESET Protect Platform/Package/testParameters.json @@ -0,0 +1,24 @@ +{ + "location": { + "type": "string", + "minLength": 1, + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Not used, but needed to pass arm-ttk test `Location-Should-Not-Be-Hardcoded`. We instead use the `workspace-location` which is derived from the LA workspace" + } + }, + "workspace-location": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "[concat('Region to deploy solution resources -- separate from location selection',parameters('location'))]" + } + }, + "workspace": { + "defaultValue": "", + "type": "string", + "metadata": { + "description": "Workspace name for Log Analytics where Microsoft Sentinel is setup" + } + } +} diff --git a/Solutions/ESET Protect Platform/SolutionMetadata.json b/Solutions/ESET Protect Platform/SolutionMetadata.json new file mode 100644 index 00000000000..5e41a55f8d0 --- /dev/null +++ b/Solutions/ESET Protect Platform/SolutionMetadata.json @@ -0,0 +1,16 @@ +{ + "publisherId": "eset", + "offerId": "eset-protect-platform-solution", + "firstPublishDate": "2024-10-15", + "lastPublishDate": "2024-10-15", + "providers": ["ESET Enterprise Integrations"], + "categories": { + "domains" : ["Security - Automation (SOAR)", "Security - Threat Protection"] + }, + "support": { + "name": "ESET Enterprise Integrations", + "email": "eset-enterpise-integration@eset.com", + "tier": "Partner", + "link": "https://help.eset.com/eset_connect/en-US/integrations.html" + } +}