From cf0a5f636fed51579d63fb75880db08905c76425 Mon Sep 17 00:00:00 2001 From: Alex Li Date: Wed, 21 Oct 2026 14:34:34 -0500 Subject: [PATCH 1/6] Add Storage Blob CCF V2 template with managed service principal auth Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 89a38227-9c14-4d51-b01d-601413d94369 --- .../Connector_StorageBlob_CCF_templateV2.json | 684 ++++++++++++++++++ 1 file changed, 684 insertions(+) create mode 100644 DataConnectors/Templates/Connector_StorageBlob_CCF_templateV2.json diff --git a/DataConnectors/Templates/Connector_StorageBlob_CCF_templateV2.json b/DataConnectors/Templates/Connector_StorageBlob_CCF_templateV2.json new file mode 100644 index 00000000000..081835585e9 --- /dev/null +++ b/DataConnectors/Templates/Connector_StorageBlob_CCF_templateV2.json @@ -0,0 +1,684 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "location": { + "defaultValue": "[resourceGroup().location]", + "minLength": 1, + "type": "String", + "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": { + "defaultValue": "", + "type": "String", + "metadata": { + "description": "[concat('Region to deploy solution resources -- separate from location selection',parameters('location'))]" + } + }, + "subscription": { + "defaultValue": "[last(split(subscription().id, '/'))]", + "type": "String", + "metadata": { + "description": "subscription id where Microsoft Sentinel is setup" + } + }, + "resourceGroupName": { + "defaultValue": "[resourceGroup().name]", + "type": "String", + "metadata": { + "description": "resource group name where Microsoft Sentinel is setup" + } + }, + "workspace": { + "defaultValue": "", + "type": "String", + "metadata": { + "description": "Workspace name for Log Analytics where Microsoft Sentinel is setup" + } + } + }, + "variables": { + "workspaceResourceId": "[resourceId('microsoft.OperationalInsights/Workspaces', parameters('workspace'))]", + "_solutionName": "Azure Storage Blob Connector // Modify to your solution name", + "_solutionVersion": "1.0.0", + "_solutionAuthor": "Microsoft // Modify to your user/company name", + "_packageIcon": "icon icon icon icon", + "_solutionId": "azuresentinel.azure-sentinel-solution-azuresentinel.azure-sentinel-azure-storage", + "dataConnectorVersionConnectorDefinition": "1.0.0", + "dataConnectorVersionConnections": "1.0.0", + "_solutionTier": "Community", + "_dataConnectorContentIdConnectorDefinition": "teastdelayBlobTemplateConnectorDefinition // Modify to your connector definition", + "dataConnectorTemplateNameConnectorDefinition": "[concat(parameters('workspace'),'-dc-',uniquestring(variables('_dataConnectorContentIdConnectorDefinition')))]", + "_dataConnectorContentIdConnections": "BlobTemplateConnections", + "dataConnectorTemplateNameConnections": "[concat(parameters('workspace'),'-dc-',uniquestring(variables('_dataConnectorContentIdConnections')))]", + "_logAnalyticsTableId1": "BlobExampleTable_CL // Modify to your table name" + }, + "resources": [ + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/', variables('dataConnectorTemplateNameConnectorDefinition'), variables('dataConnectorVersionConnectorDefinition'))]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "contentId": "[variables('_dataConnectorContentIdConnectorDefinition')]", + "displayName": "[concat(variables('_solutionName'), variables('dataConnectorTemplateNameConnectorDefinition'))]", + "contentKind": "DataConnector", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('dataConnectorVersionConnectorDefinition')]", + "parameters": {}, + "variables": {}, + "resources": [ + { + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('DataConnector-', variables('_dataConnectorContentIdConnectorDefinition')))]", + "apiVersion": "2022-01-01-preview", + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "properties": { + "parentId": "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/dataConnectorDefinitions', variables('_dataConnectorContentIdConnectorDefinition'))]", + "contentId": "[variables('_dataConnectorContentIdConnectorDefinition')]", + "kind": "DataConnector", + "version": "[variables('dataConnectorVersionConnectorDefinition')]", + "source": { + "sourceId": "[variables('_solutionId')]", + "name": "[variables('_solutionName')]", + "kind": "Solution" + }, + "author": { + "name": "[variables('_solutionAuthor')]" + }, + "support": { + "name": "[variables('_solutionAuthor')]", + "tier": "[variables('_solutionTier')]" + }, + "dependencies": { + "criteria": [ + { + "version": "[variables('dataConnectorVersionConnections')]", + "contentId": "[variables('_dataConnectorContentIdConnections')]", + "kind": "ResourcesDataConnector" + } + ] + } + } + }, + { + "name": "[variables('_logAnalyticsTableId1')]", + "apiVersion": "2021-03-01-privatepreview", + "type": "Microsoft.OperationalInsights/workspaces/tables", + "location": "[parameters('workspace-location')]", + "kind": null, + "properties": { + "schema": { + "name": "[variables('_logAnalyticsTableId1')]", + "columns": [ + { + "name": "TimeGenerated", + "type": "datetime", + "isDefaultDisplay": true, + "description": "The timestamp (UTC) reflecting the time in which the event was generated." + }, + { + "name": "Data // Modify with your table columns", + "type": "string", + "description": "The data payload." + } + ] + } + }, + "dependsOn": null + }, + { + "name": "StorageBlob", + "apiVersion": "2021-09-01-preview", + "type": "Microsoft.Insights/dataCollectionRules", + "location": "[parameters('workspace-location')]", + "kind": null, + "properties": { + "streamDeclarations": { + "Custom-StorageBlob // Modify your stream name": { + "columns": [ + { + "name": "TimeGenerated", + "type": "datetime", + "isDefaultDisplay": true, + "description": "The timestamp (UTC) reflecting the time in which the event was generated." + }, + { + "name": "Data", + "type": "string", + "description": "The data payload." + } + ] + } + }, + "destinations": { + "logAnalytics": [ + { + "workspaceResourceId": "[variables('workspaceResourceId')]", + "name": "clv2ws1" + } + ] + }, + "dataFlows": [ + { + "streams": [ + "Custom-StorageBlob // Modify your stream name (same as 143)" + ], + "destinations": [ + "clv2ws1" + ], + "transformKql": "source | extend TimeGenerated = now()", + "outputStream": "[concat('Custom-',variables('_logAnalyticsTableId1'))]" + } + ], + "dataCollectionEndpointId": "[concat('/subscriptions/',parameters('subscription'),'/resourceGroups/',parameters('resourceGroupName'),'/providers/Microsoft.Insights/dataCollectionEndpoints/',parameters('workspace'))]" + }, + "dependsOn": null + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "contentProductId": "[concat(substring(variables('_solutionId'), 0, 50),'-','dc','-', uniqueString(concat(variables('_solutionId'),'-','DataConnector','-',variables('_dataConnectorContentIdConnectorDefinition'),'-', variables('dataConnectorVersionConnectorDefinition'))))]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "version": "[variables('_solutionVersion')]" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/dataConnectorDefinitions", + "apiVersion": "2022-09-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',variables('_dataConnectorContentIdConnectorDefinition'))]", + "location": "[parameters('workspace-location')]", + "kind": "Customizable", + "properties": { + "connectorUiConfig": { + "id": "AzureStorageBlobConnector", + "title": "AzureStorageBlobConnector // modify to your title", + "publisher": "Microsoft // Modify to your user / company name", + "descriptionMarkdown": "AzureStorageBlobConnector // Modify to your description", + "graphQueriesTableName": "[variables('_logAnalyticsTableId1')]", + "graphQueries": [ + { + "metricName": "Total logs received", + "legend": "Azure Storage Blob Logs", + "baseQuery": "{{graphQueriesTableName}}" + } + ], + "sampleQueries": [ + { + "description": "Get Sample of Azure Storage Blob Logs", + "query": "{{graphQueriesTableName}}\n | take 10" + } + ], + "dataTypes": [ + { + "name": "{{graphQueriesTableName}}", + "lastDataReceivedQuery": "{{graphQueriesTableName}}\n|summarize Time = max(TimeGenerated)\n|where isnotempty(Time)" + } + ], + "connectivityCriteria": [ + { + "type": "HasDataConnectors", + "value": null + } + ], + "availability": { + "status": 1, + "isPreview": false + }, + "permissions": { + "tenant": null, + "licenses": null, + "resourceProvider": [ + { + "provider": "Microsoft.OperationalInsights/workspaces", + "permissionsDisplayText": "Read and Write permissions are required.", + "providerDisplayName": "Workspace", + "scope": "Workspace", + "requiredPermissions": { + "read": true, + "write": true, + "delete": true, + "action": false + } + }, + { + "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": { + "read": false, + "write": false, + "delete": false, + "action": true + } + } + ], + "customs": [ + { + "name": "Subscription permissions", + "description": "You need permissions to create the data flow resources: \n- storage queues (notification queue and dead-letter queue) \n- event grid topic and subscription (to send 'blob created event' notifications to the notification queue) \n- role assignments (to grant access for sentinel app to the blob container and the storage queues.)" + }, + { + "name": "Collecting data from __ to your blob container", + "description": "Follow the steps in the [documentation](https://some-guide.net) for collecting data from __ to your blob container." + } + ] + }, + "instructionSteps": [ + { + "title": "Connect Azure Storage Blob Logs to Microsoft Sentinel", + "description": "To enable the Azure Storage Blob Logs for Microsoft Sentinel, provide the required information below and click on Connect.\n>", + "instructions": [ + { + "parameters": { + "tenantId": "[subscription().tenantId]", + "name": "principalId", + "appId": "7ae5462d-d9d1-42f6-93ca-198d7b0ca997 // Modify to match the appId specific to the codeless connector environment" + }, + "type": "ServicePrincipalIDTextBox_test" + }, + { + "parameters": { + "label": "The blob container URL you want to collect data from", + "type": "text", + "name": "blobContainerUri" + }, + "type": "Textbox" + }, + { + "parameters": { + "label": "The blobs folder name in the container. Optional", + "type": "text", + "name": "blobFolderName" + }, + "type": "Textbox" + }, + { + "parameters": { + "label": "The blob container's storage account location", + "type": "text", + "name": "StorageAccountLocation" + }, + "type": "Textbox" + }, + { + "parameters": { + "label": "The blob container's storage account resource group name", + "type": "text", + "name": "StorageAccountResourceGroupName" + }, + "type": "Textbox" + }, + { + "parameters": { + "label": "The blob container's storage account subscription id", + "type": "text", + "name": "StorageAccountSubscription" + }, + "type": "Textbox" + }, + { + "parameters": { + "label": "The event grid topic name of the blob container's storage account if exist. else keep empty.", + "description": "The data flow using event grid to send 'blob-created event' notifications. There could be only one event grid topic for each storage account.\nGo to your blob container's storage account and look in the 'Events' section. If you already have a topic, please provide it's name. Else, keep the text box empty.", + "placeholder": "", + "type": "text", + "name": "EGSystemTopicName" + }, + "type": "Textbox" + }, + { + "parameters": { + "label": "toggle", + "name": "toggle" + }, + "type": "ConnectionToggleButton" + } + ], + "innerSteps": null + } + ], + "isConnectivityCriteriasMatchSome": false + } + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "apiVersion": "2022-01-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('DataConnector-', variables('_dataConnectorContentIdConnectorDefinition')))]", + "properties": { + "parentId": "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/dataConnectorDefinitions', variables('_dataConnectorContentIdConnectorDefinition'))]", + "contentId": "[variables('_dataConnectorContentIdConnectorDefinition')]", + "kind": "DataConnector", + "version": "[variables('dataConnectorVersionConnectorDefinition')]", + "source": { + "sourceId": "[variables('_solutionId')]", + "name": "[variables('_solutionName')]", + "kind": "Solution" + }, + "author": { + "name": "[variables('_solutionAuthor')]" + }, + "support": { + "name": "[variables('_solutionAuthor')]", + "tier": "[variables('_solutionTier')]" + }, + "dependencies": { + "criteria": [ + { + "version": "[variables('dataConnectorVersionConnections')]", + "contentId": "[variables('_dataConnectorContentIdConnections')]", + "kind": "ResourcesDataConnector" + } + ] + } + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/', variables('dataConnectorTemplateNameConnections'), variables('dataConnectorVersionConnections'))]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "contentId": "[variables('_dataConnectorContentIdConnections')]", + "displayName": "[concat(variables('_solutionName'), variables('dataConnectorTemplateNameConnections'))]", + "contentKind": "ResourcesDataConnector", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('dataConnectorVersionConnections')]", + "parameters": { + "connectorDefinitionName": { + "defaultValue": "connectorDefinitionName", + "type": "string", + "minLength": 1 + }, + "workspaceName": { + "defaultValue": "[parameters('workspace')]", + "type": "string" + }, + "dcrConfig": { + "defaultValue": { + "dataCollectionEndpoint": "data collection Endpoint", + "dataCollectionRuleImmutableId": "data collection rule immutableId" + }, + "type": "object" + }, + "principalId": { + "defaultValue": "principalId", + "type": "string" + }, + "blobContainerUri": { + "defaultValue": "blobContainerUri", + "type": "string" + }, + "blobFolderName": { + "defaultValue": "", + "type": "string" + }, + "StorageAccountLocation": { + "defaultValue": "StorageAccountLocation", + "type": "string" + }, + "StorageAccountResourceGroupName": { + "defaultValue": "StorageAccountResourceGroupName", + "type": "string" + }, + "StorageAccountSubscription": { + "defaultValue": "StorageAccountSubscription", + "type": "string" + }, + "EGSystemTopicName": { + "defaultValue": "", + "type": "string" + } + }, + "variables": { + "_dataConnectorContentIdConnections": "[variables('_dataConnectorContentIdConnections')]", + "connectorName": "storageblob", + "storageAccountName": "[[split(split(parameters('blobContainerUri'), 'https://')[1], '.blob.core.windows.net')[0]]", + "blobContainerName": "[[split(split(parameters('blobContainerUri'), '.blob.core.windows.net/')[1], '/')[0]]", + "queueName": "[[concat(variables('connectorName'), '-notification')]", + "dlqName": "[[concat(variables('connectorName'), '-dlq')]", + "ResourcesIdPrefix": "[[format('/subscriptions/{0}/resourceGroups/{1}/providers', parameters('StorageAccountSubscription'), parameters('StorageAccountResourceGroupName'))]", + "storageAccountId": "[[format('{0}/Microsoft.Storage/storageAccounts/{1}', variables('ResourcesIdPrefix'), variables('storageAccountName'))]", + "notificationQueueResourceId": "[[format('{0}/Microsoft.Storage/storageAccounts/{1}/queueServices//default/queues/{2}', variables('ResourcesIdPrefix'), variables('storageAccountName'), variables('queueName'))]", + "dlqResourceId": "[[format('{0}/Microsoft.Storage/storageAccounts/{1}/queueServices//default/queues/{2}', variables('ResourcesIdPrefix'), variables('storageAccountName'), variables('dlqName'))]", + "EGSystemTopicDefaultName": "[[format('eg-system-topic-{0}-{1}', variables('connectorName'), parameters('workspaceName'))]", + "EGSystemTopicName": "[[if(empty(parameters('EGSystemTopicName')), variables('EGSystemTopicDefaultName'), parameters('EGSystemTopicName'))]", + "EGTopicResourceId": "[[format('{0}/Microsoft.EventGrid/systemTopics/{1}', variables('ResourcesIdPrefix'), variables('EGSystemTopicName'))]", + "EgSubscriptionName": "[[format('{0}-{1}', variables('connectorName'), 'blobcreatedevents')]", + "EgSubscriptionResourceId": "[[format('{0}/Microsoft.EventGrid/systemTopics/{1}/eventSubscriptions/{2}', variables('ResourcesIdPrefix'), variables('EGSystemTopicName'), variables('EgSubscriptionName'))]", + "storageBlobContributorRoleId": "[[format('/subscriptions/{0}/providers/Microsoft.Authorization/roleDefinitions/ba92f5b4-2d11-453d-a403-e96b0029c9fe', parameters('StorageAccountSubscription'))]", + "storageQueueContributorRoleId": "[[format('/subscriptions/{0}/providers/Microsoft.Authorization/roleDefinitions/974c5e8b-45b9-4653-ba55-5f855dd0fb88', parameters('StorageAccountSubscription'))]", + "blobRaGuid": "[[guid(variables('storageAccountName'), variables('blobContainerName'))]", + "notificationQueueRaGuid": "[[guid(variables('storageAccountName'), variables('queueName'))]", + "dlqRaGuid": "[[guid(variables('storageAccountName'), variables('dlqName'))]", + "blobRoleAssignmentResourceId": "[[format('{0}/Microsoft.Storage/storageAccounts/{1}/blobServices/default/containers/{2}/providers/Microsoft.Authorization/roleAssignments/{3}', variables('ResourcesIdPrefix'), variables('storageAccountName'), variables('blobContainerName'),variables('blobRaGuid'))]", + "notificationQueueRoleAssignmentResourceId": "[[format('{0}/Microsoft.Storage/storageAccounts/{1}/queueServices/default/queues/{2}/providers/Microsoft.Authorization/roleAssignments/{3}', variables('ResourcesIdPrefix'), variables('storageAccountName'), variables('queueName'),variables('notificationQueueRaGuid'))]", + "dlqRoleAssignmentResourceId": "[[format('{0}/Microsoft.Storage/storageAccounts/{1}/queueServices/default/queues/{2}/providers/Microsoft.Authorization/roleAssignments/{3}', variables('ResourcesIdPrefix'), variables('storageAccountName'), variables('dlqName'),variables('dlqRaGuid'))]", + "nestedDeploymentName": "CreateDataFlowResources", + "nestedDeploymentId": "[[format('{0}/Microsoft.Resources/deployments/{1}', variables('ResourcesIdPrefix'), variables('nestedDeploymentName'))]" + }, + "resources": [ + { + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('DataConnector-', variables('_dataConnectorContentIdConnections')))]", + "apiVersion": "2022-01-01-preview", + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "properties": { + "parentId": "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/dataConnectors', variables('_dataConnectorContentIdConnections'))]", + "contentId": "[variables('_dataConnectorContentIdConnections')]", + "kind": "ResourcesDataConnector", + "version": "[variables('dataConnectorVersionConnections')]", + "source": { + "sourceId": "[variables('_solutionId')]", + "name": "[variables('_solutionName')]", + "kind": "Solution" + }, + "author": { + "name": "[variables('_solutionAuthor')]" + }, + "support": { + "name": "[variables('_solutionAuthor')]", + "tier": "[variables('_solutionTier')]" + } + } + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2021-04-01", + "name": "[[variables('nestedDeploymentName')]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [ + { + "type": "Microsoft.Storage/storageAccounts/queueServices/queues", + "apiVersion": "2021-04-01", + "name": "[[concat(variables('storageAccountName'), '/default/', variables('queueName'))]", + "dependsOn": [], + "properties": {} + }, + { + "type": "Microsoft.Storage/storageAccounts/queueServices/queues", + "apiVersion": "2021-04-01", + "name": "[[concat(variables('storageAccountName'), '/default/', variables('dlqName'))]", + "dependsOn": [], + "properties": {} + }, + { + "type": "Microsoft.EventGrid/systemTopics", + "apiVersion": "2022-06-15", + "name": "[[variables('EGSystemTopicName')]", + "location": "[[parameters('StorageAccountLocation')]", + "properties": { + "provisioningState": "Succeeded", + "source": "[[variables('storageAccountId')]", + "topicType": "microsoft.storage.storageaccounts", + "metricResourceId": "0cefe8d9-3269-4f68-a44e-46a4fc26e4a9" + }, + "condition": "[[empty(parameters('EGSystemTopicName'))]" + }, + { + "type": "Microsoft.EventGrid/systemTopics/eventSubscriptions", + "apiVersion": "2023-12-15-preview", + "name": "[[format('{0}/{1}', variables('EGSystemTopicName'), variables('EgSubscriptionName'))]", + "dependsOn": [ + "[[variables('EGTopicResourceId')]", + "[[variables('notificationQueueResourceId')]" + ], + "properties": { + "destination": { + "endpointType": "StorageQueue", + "properties": { + "queueName": "[[variables('queueName')]", + "resourceId": "[[variables('storageAccountId')]" + } + }, + "filter": { + "includedEventTypes": [ + "Microsoft.Storage.BlobCreated" + ], + "subjectBeginsWith": "[[format('{0}/{1}/blobs/{2}', '/blobServices/default/containers', variables('blobContainerName'), if(empty(parameters('blobFolderName')), '', concat(parameters('blobFolderName'),'/')))]" + } + } + }, + { + "type": "Microsoft.Storage/storageAccounts/blobServices/containers/providers/roleAssignments", + "apiVersion": "2018-01-01-preview", + "name": "[[concat(variables('storageAccountName'), '/default/', variables('blobContainerName'), '/Microsoft.Authorization/', variables('blobRaGuid'))]", + "properties": { + "roleDefinitionId": "[[variables('storageBlobContributorRoleId')]", + "principalId": "[[parameters('principalId')]" + } + }, + { + "type": "Microsoft.Storage/storageAccounts/queueServices/queues/providers/roleAssignments", + "apiVersion": "2018-01-01-preview", + "name": "[[concat(variables('storageAccountName'), '/default/', variables('queueName'), '/Microsoft.Authorization/', variables('notificationQueueRaGuid'))]", + "dependsOn": [ + "[[variables('notificationQueueResourceId')]" + ], + "properties": { + "roleDefinitionId": "[[variables('storageQueueContributorRoleId')]", + "principalId": "[[parameters('principalId')]" + } + }, + { + "type": "Microsoft.Storage/storageAccounts/queueServices/queues/providers/roleAssignments", + "apiVersion": "2018-01-01-preview", + "name": "[[concat(variables('storageAccountName'), '/default/', variables('dlqName'), '/Microsoft.Authorization/', variables('dlqRaGuid'))]", + "dependsOn": [ + "[[variables('dlqResourceId')]" + ], + "properties": { + "roleDefinitionId": "[[variables('storageQueueContributorRoleId')]", + "principalId": "[[parameters('principalId')]" + } + } + ] + } + }, + "subscriptionId": "[[parameters('StorageAccountSubscription')]", + "resourceGroup": "[[parameters('StorageAccountResourceGroupName')]" + }, + { + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/', 'CustomAzureStorageBlob')]", + "apiVersion": "2022-12-01-preview", + "type": "Microsoft.OperationalInsights/workspaces/providers/dataConnectors", + "location": "[parameters('workspace-location')]", + "kind": "StorageAccountBlobContainer", + "properties": { + "connectorDefinitionName": "[[parameters('connectorDefinitionName')]", + "dcrConfig": { + "streamName": "Custom-StorageBlob // Modify your stream name (same as 143)", + "dataCollectionEndpoint": "[[parameters('dcrConfig').dataCollectionEndpoint]", + "dataCollectionRuleImmutableId": "[[parameters('dcrConfig').dataCollectionRuleImmutableId]" + }, + "auth": { + "type": "ManagedServicePrincipal" + }, + "request": { + "QueueUri": "[[concat('https://', variables('storageAccountName'), '.queue.core.windows.net/', variables('queueName'))]", + "DlqUri": "[[concat('https://', variables('storageAccountName'), '.queue.core.windows.net/', variables('dlqName'))]" + }, + "response": { + "eventsJsonPaths": [ + "$" + ], + "format": "json", + "isGzipCompressed": false + } + }, + "dependsOn": [] + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "contentProductId": "[concat(substring(variables('_solutionId'), 0, 50),'-','rdc','-', uniqueString(concat(variables('_solutionId'),'-','ResourcesDataConnector','-',variables('_dataConnectorContentIdConnections'),'-', variables('dataConnectorVersionConnections'))))]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "version": "[variables('_solutionVersion')]" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentPackages", + "apiVersion": "2023-04-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/', variables('_solutionId'))]", + "location": "[parameters('workspace-location')]", + "properties": { + "version": "[variables('_solutionVersion')]", + "kind": "Solution", + "contentSchemaVersion": "3.0.0", + "contentId": "[variables('_solutionId')]", + "source": { + "kind": "Solution", + "name": "[variables('_solutionName')]", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "[variables('_solutionAuthor')]" + }, + "support": { + "name": "[variables('_solutionAuthor')]" + }, + "dependencies": { + "operator": "AND", + "criteria": [ + { + "kind": "DataConnector", + "contentId": "[variables('dataConnectorVersionConnectorDefinition')]", + "version": "[variables('_dataConnectorContentIdConnectorDefinition')]" + } + ] + }, + "firstPublishDate": "2022-06-24", + "providers": [ + "[variables('_solutionAuthor')]" + ], + "contentKind": "Solution", + "packageId": "[variables('_solutionId')]", + "contentProductId": "[concat(substring(variables('_solutionId'), 0, 50),'-','sl','-', uniqueString(concat(variables('_solutionId'),'-','Solution','-',variables('_solutionId'),'-', variables('_solutionVersion'))))]", + "displayName": "[variables('_solutionName')]", + "publisherDisplayName": "[variables('_solutionId')]", + "descriptionHtml": "AzureStorageBlobConnector", + "icon": "[variables('_packageIcon')]" + } + } + ] +} \ No newline at end of file From c9e1edbac034eb94be83a7412a5f7334c04746d3 Mon Sep 17 00:00:00 2001 From: TwistedAlex <40569707+TwistedAlex@users.noreply.github.com> Date: Thu, 22 Oct 2026 15:40:44 -0500 Subject: [PATCH 2/6] Remove explanatory suffix from FPA application ID Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: cb82e9b1-2045-4766-bc2b-1bd92d87f0e7 --- .../Templates/Connector_StorageBlob_CCF_templateV2.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DataConnectors/Templates/Connector_StorageBlob_CCF_templateV2.json b/DataConnectors/Templates/Connector_StorageBlob_CCF_templateV2.json index 081835585e9..19392625490 100644 --- a/DataConnectors/Templates/Connector_StorageBlob_CCF_templateV2.json +++ b/DataConnectors/Templates/Connector_StorageBlob_CCF_templateV2.json @@ -282,7 +282,7 @@ "parameters": { "tenantId": "[subscription().tenantId]", "name": "principalId", - "appId": "7ae5462d-d9d1-42f6-93ca-198d7b0ca997 // Modify to match the appId specific to the codeless connector environment" + "appId": "7ae5462d-d9d1-42f6-93ca-198d7b0ca997" }, "type": "ServicePrincipalIDTextBox_test" }, From 0219cdeb427d6d9251787bf1fec9c52674b1932f Mon Sep 17 00:00:00 2001 From: TwistedAlex <40569707+TwistedAlex@users.noreply.github.com> Date: Thu, 22 Oct 2026 15:54:25 -0500 Subject: [PATCH 3/6] Make FPA storage role assignments migration-safe Derive assignment names from normalized full scope, tenant-local principal ID, and role-definition ID. Use scoped Microsoft.Authorization/roleAssignments resources with API 2022-04-01 and principalType ServicePrincipal. Preserve incremental deployment and existing grants. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: cb82e9b1-2045-4766-bc2b-1bd92d87f0e7 --- .../Connector_StorageBlob_CCF_templateV2.json | 47 +++++++++++-------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/DataConnectors/Templates/Connector_StorageBlob_CCF_templateV2.json b/DataConnectors/Templates/Connector_StorageBlob_CCF_templateV2.json index 19392625490..88a8ee214a4 100644 --- a/DataConnectors/Templates/Connector_StorageBlob_CCF_templateV2.json +++ b/DataConnectors/Templates/Connector_StorageBlob_CCF_templateV2.json @@ -453,8 +453,9 @@ "dlqName": "[[concat(variables('connectorName'), '-dlq')]", "ResourcesIdPrefix": "[[format('/subscriptions/{0}/resourceGroups/{1}/providers', parameters('StorageAccountSubscription'), parameters('StorageAccountResourceGroupName'))]", "storageAccountId": "[[format('{0}/Microsoft.Storage/storageAccounts/{1}', variables('ResourcesIdPrefix'), variables('storageAccountName'))]", - "notificationQueueResourceId": "[[format('{0}/Microsoft.Storage/storageAccounts/{1}/queueServices//default/queues/{2}', variables('ResourcesIdPrefix'), variables('storageAccountName'), variables('queueName'))]", - "dlqResourceId": "[[format('{0}/Microsoft.Storage/storageAccounts/{1}/queueServices//default/queues/{2}', variables('ResourcesIdPrefix'), variables('storageAccountName'), variables('dlqName'))]", + "blobContainerResourceId": "[[format('{0}/blobServices/default/containers/{1}', variables('storageAccountId'), variables('blobContainerName'))]", + "notificationQueueResourceId": "[[format('{0}/queueServices/default/queues/{1}', variables('storageAccountId'), variables('queueName'))]", + "dlqResourceId": "[[format('{0}/queueServices/default/queues/{1}', variables('storageAccountId'), variables('dlqName'))]", "EGSystemTopicDefaultName": "[[format('eg-system-topic-{0}-{1}', variables('connectorName'), parameters('workspaceName'))]", "EGSystemTopicName": "[[if(empty(parameters('EGSystemTopicName')), variables('EGSystemTopicDefaultName'), parameters('EGSystemTopicName'))]", "EGTopicResourceId": "[[format('{0}/Microsoft.EventGrid/systemTopics/{1}', variables('ResourcesIdPrefix'), variables('EGSystemTopicName'))]", @@ -462,12 +463,12 @@ "EgSubscriptionResourceId": "[[format('{0}/Microsoft.EventGrid/systemTopics/{1}/eventSubscriptions/{2}', variables('ResourcesIdPrefix'), variables('EGSystemTopicName'), variables('EgSubscriptionName'))]", "storageBlobContributorRoleId": "[[format('/subscriptions/{0}/providers/Microsoft.Authorization/roleDefinitions/ba92f5b4-2d11-453d-a403-e96b0029c9fe', parameters('StorageAccountSubscription'))]", "storageQueueContributorRoleId": "[[format('/subscriptions/{0}/providers/Microsoft.Authorization/roleDefinitions/974c5e8b-45b9-4653-ba55-5f855dd0fb88', parameters('StorageAccountSubscription'))]", - "blobRaGuid": "[[guid(variables('storageAccountName'), variables('blobContainerName'))]", - "notificationQueueRaGuid": "[[guid(variables('storageAccountName'), variables('queueName'))]", - "dlqRaGuid": "[[guid(variables('storageAccountName'), variables('dlqName'))]", - "blobRoleAssignmentResourceId": "[[format('{0}/Microsoft.Storage/storageAccounts/{1}/blobServices/default/containers/{2}/providers/Microsoft.Authorization/roleAssignments/{3}', variables('ResourcesIdPrefix'), variables('storageAccountName'), variables('blobContainerName'),variables('blobRaGuid'))]", - "notificationQueueRoleAssignmentResourceId": "[[format('{0}/Microsoft.Storage/storageAccounts/{1}/queueServices/default/queues/{2}/providers/Microsoft.Authorization/roleAssignments/{3}', variables('ResourcesIdPrefix'), variables('storageAccountName'), variables('queueName'),variables('notificationQueueRaGuid'))]", - "dlqRoleAssignmentResourceId": "[[format('{0}/Microsoft.Storage/storageAccounts/{1}/queueServices/default/queues/{2}/providers/Microsoft.Authorization/roleAssignments/{3}', variables('ResourcesIdPrefix'), variables('storageAccountName'), variables('dlqName'),variables('dlqRaGuid'))]", + "blobRaGuid": "[[guid(toLower(variables('blobContainerResourceId')), toLower(parameters('principalId')), toLower(variables('storageBlobContributorRoleId')))]", + "notificationQueueRaGuid": "[[guid(toLower(variables('notificationQueueResourceId')), toLower(parameters('principalId')), toLower(variables('storageQueueContributorRoleId')))]", + "dlqRaGuid": "[[guid(toLower(variables('dlqResourceId')), toLower(parameters('principalId')), toLower(variables('storageQueueContributorRoleId')))]", + "blobRoleAssignmentResourceId": "[[format('{0}/providers/Microsoft.Authorization/roleAssignments/{1}', variables('blobContainerResourceId'), variables('blobRaGuid'))]", + "notificationQueueRoleAssignmentResourceId": "[[format('{0}/providers/Microsoft.Authorization/roleAssignments/{1}', variables('notificationQueueResourceId'), variables('notificationQueueRaGuid'))]", + "dlqRoleAssignmentResourceId": "[[format('{0}/providers/Microsoft.Authorization/roleAssignments/{1}', variables('dlqResourceId'), variables('dlqRaGuid'))]", "nestedDeploymentName": "CreateDataFlowResources", "nestedDeploymentId": "[[format('{0}/Microsoft.Resources/deployments/{1}', variables('ResourcesIdPrefix'), variables('nestedDeploymentName'))]" }, @@ -557,36 +558,42 @@ } }, { - "type": "Microsoft.Storage/storageAccounts/blobServices/containers/providers/roleAssignments", - "apiVersion": "2018-01-01-preview", - "name": "[[concat(variables('storageAccountName'), '/default/', variables('blobContainerName'), '/Microsoft.Authorization/', variables('blobRaGuid'))]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "name": "[[variables('blobRaGuid')]", + "scope": "[[variables('blobContainerResourceId')]", "properties": { "roleDefinitionId": "[[variables('storageBlobContributorRoleId')]", - "principalId": "[[parameters('principalId')]" + "principalId": "[[parameters('principalId')]", + "principalType": "ServicePrincipal" } }, { - "type": "Microsoft.Storage/storageAccounts/queueServices/queues/providers/roleAssignments", - "apiVersion": "2018-01-01-preview", - "name": "[[concat(variables('storageAccountName'), '/default/', variables('queueName'), '/Microsoft.Authorization/', variables('notificationQueueRaGuid'))]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "name": "[[variables('notificationQueueRaGuid')]", + "scope": "[[variables('notificationQueueResourceId')]", "dependsOn": [ "[[variables('notificationQueueResourceId')]" ], "properties": { "roleDefinitionId": "[[variables('storageQueueContributorRoleId')]", - "principalId": "[[parameters('principalId')]" + "principalId": "[[parameters('principalId')]", + "principalType": "ServicePrincipal" } }, { - "type": "Microsoft.Storage/storageAccounts/queueServices/queues/providers/roleAssignments", - "apiVersion": "2018-01-01-preview", - "name": "[[concat(variables('storageAccountName'), '/default/', variables('dlqName'), '/Microsoft.Authorization/', variables('dlqRaGuid'))]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "name": "[[variables('dlqRaGuid')]", + "scope": "[[variables('dlqResourceId')]", "dependsOn": [ "[[variables('dlqResourceId')]" ], "properties": { "roleDefinitionId": "[[variables('storageQueueContributorRoleId')]", - "principalId": "[[parameters('principalId')]" + "principalId": "[[parameters('principalId')]", + "principalType": "ServicePrincipal" } } ] From 9574d971fde076811d0f861e4469dce5f1e591fe Mon Sep 17 00:00:00 2001 From: TwistedAlex <40569707+TwistedAlex@users.noreply.github.com> Date: Fri, 23 Oct 2026 14:21:58 -0500 Subject: [PATCH 4/6] Enable identity delivery and multiple Storage Blob connections Add storage account request context, preserve Event Grid topic identity state, and support account-scoped sender grants with create/reuse modes. Isolate connection resources behind the grid and context-pane UI while retaining first-party collector authentication and migration-safe role assignments. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Connector_StorageBlob_CCF_templateV2.json | 549 ++++++++++++++---- .../Data Connectors Template Guidance.md | 76 +++ 2 files changed, 519 insertions(+), 106 deletions(-) diff --git a/DataConnectors/Templates/Connector_StorageBlob_CCF_templateV2.json b/DataConnectors/Templates/Connector_StorageBlob_CCF_templateV2.json index 88a8ee214a4..bfaab85896b 100644 --- a/DataConnectors/Templates/Connector_StorageBlob_CCF_templateV2.json +++ b/DataConnectors/Templates/Connector_StorageBlob_CCF_templateV2.json @@ -265,7 +265,11 @@ "customs": [ { "name": "Subscription permissions", - "description": "You need permissions to create the data flow resources: \n- storage queues (notification queue and dead-letter queue) \n- event grid topic and subscription (to send 'blob created event' notifications to the notification queue) \n- role assignments (to grant access for sentinel app to the blob container and the storage queues.)" + "description": "You need Contributor permissions on the target resource groups to deploy the data flow resources, and Owner or User Access Administrator permissions on the storage account to create role assignments. The deployment creates notification and dead-letter queues, creates or updates an Event Grid system topic and event subscription, and grants the first-party collector access to the blob container and queues. When Create is selected, it also grants the Event Grid system topic's managed identity Storage Queue Data Message Sender at storage-account scope. The Microsoft.EventGrid resource provider must be registered in the storage account's subscription." + }, + { + "name": "Event Grid delivery identity", + "description": "The connector enables system-assigned managed identity on a new or supplied existing Event Grid system topic. An existing topic must use the selected storage account as its source; its tags, attached user-assigned identities, and any existing system-assigned principal are preserved. The generated event subscription always uses system-assigned identity delivery. Choose Create to grant Storage Queue Data Message Sender on the destination storage account, or Use existing only when that same identity already has the required account-scoped permission. This identity is separate from the first-party collector application. The connector does not configure storage networking or Network Security Perimeter rules." }, { "name": "Collecting data from __ to your blob container", @@ -276,75 +280,153 @@ "instructionSteps": [ { "title": "Connect Azure Storage Blob Logs to Microsoft Sentinel", - "description": "To enable the Azure Storage Blob Logs for Microsoft Sentinel, provide the required information below and click on Connect.\n>", + "description": "Use the connection grid to add and manage Azure Storage Blob connections.", "instructions": [ { + "type": "DataConnectorsGrid", "parameters": { - "tenantId": "[subscription().tenantId]", - "name": "principalId", - "appId": "7ae5462d-d9d1-42f6-93ca-198d7b0ca997" - }, - "type": "ServicePrincipalIDTextBox_test" - }, - { - "parameters": { - "label": "The blob container URL you want to collect data from", - "type": "text", - "name": "blobContainerUri" - }, - "type": "Textbox" - }, - { - "parameters": { - "label": "The blobs folder name in the container. Optional", - "type": "text", - "name": "blobFolderName" - }, - "type": "Textbox" - }, - { - "parameters": { - "label": "The blob container's storage account location", - "type": "text", - "name": "StorageAccountLocation" - }, - "type": "Textbox" - }, - { - "parameters": { - "label": "The blob container's storage account resource group name", - "type": "text", - "name": "StorageAccountResourceGroupName" - }, - "type": "Textbox" - }, - { - "parameters": { - "label": "The blob container's storage account subscription id", - "type": "text", - "name": "StorageAccountSubscription" - }, - "type": "Textbox" - }, - { - "parameters": { - "label": "The event grid topic name of the blob container's storage account if exist. else keep empty.", - "description": "The data flow using event grid to send 'blob-created event' notifications. There could be only one event grid topic for each storage account.\nGo to your blob container's storage account and look in the 'Events' section. If you already have a topic, please provide it's name. Else, keep the text box empty.", - "placeholder": "", - "type": "text", - "name": "EGSystemTopicName" - }, - "type": "Textbox" + "mapping": [ + { + "columnName": "Connection Alias", + "columnValue": "properties.addOnAttributes.friendlyName" + } + ], + "menuItems": [ + "DeleteConnector" + ] + } }, { + "type": "ContextPane", "parameters": { - "label": "toggle", - "name": "toggle" - }, - "type": "ConnectionToggleButton" + "isPrimary": true, + "label": "Add Connection", + "title": "Connect Azure Storage Blob Logs to Microsoft Sentinel", + "subtitle": "Provide the storage information for a new connection.", + "contextPaneType": "DataConnectorsContextPane", + "instructionSteps": [ + { + "title": "Connection configuration", + "instructions": [ + { + "type": "ServicePrincipalIDTextBox_test", + "parameters": { + "tenantId": "[subscription().tenantId]", + "name": "principalId", + "appId": "7ae5462d-d9d1-42f6-93ca-198d7b0ca997" + } + }, + { + "type": "Textbox", + "parameters": { + "label": "Connection alias", + "placeholder": "e.g. Production", + "type": "text", + "name": "friendlyName", + "validations": { + "required": true + } + } + }, + { + "type": "InfoMessage", + "parameters": { + "text": "Use a unique alias for each connection in this workspace. Keep the alias unchanged when retrying. Avoid overlapping container and folder selections unless duplicate ingestion is intended.", + "visible": true, + "inline": true + } + }, + { + "type": "Textbox", + "parameters": { + "label": "The blob container URL you want to collect data from", + "type": "text", + "name": "blobContainerUri" + } + }, + { + "type": "Textbox", + "parameters": { + "label": "The blob folder name in the container. Optional.", + "type": "text", + "name": "blobFolderName" + } + }, + { + "type": "Textbox", + "parameters": { + "label": "The blob container's storage account location", + "type": "text", + "name": "StorageAccountLocation" + } + }, + { + "type": "Textbox", + "parameters": { + "label": "The blob container's storage account resource group name", + "type": "text", + "name": "StorageAccountResourceGroupName" + } + }, + { + "type": "Textbox", + "parameters": { + "label": "The blob container's storage account subscription ID", + "type": "text", + "name": "StorageAccountSubscription" + } + }, + { + "type": "Textbox", + "parameters": { + "label": "Existing Event Grid system topic name. Optional.", + "type": "text", + "name": "EGSystemTopicName", + "validations": { + "required": false + } + } + }, + { + "type": "InfoMessage", + "parameters": { + "text": "Only one Event Grid system topic can exist per storage account. If one exists, including one created by another connection, enter its exact name. The connector preserves existing tags and identities while enabling system-assigned identity. Otherwise, leave empty to create a topic. Each connection gets its own event subscription and queues.", + "visible": true, + "inline": true + } + }, + { + "type": "Radio", + "parameters": { + "title": "Event Grid Storage Queue Data Message Sender assignment", + "name": "eventGridRbacMode", + "validations": { + "required": true + }, + "options": [ + { + "label": "Create the required storage-account role assignment", + "value": "CreateRoleAssignment" + }, + { + "label": "Use an existing storage-account role assignment", + "value": "UseExistingRoleAssignment" + } + ] + } + }, + { + "type": "Markdown", + "parameters": { + "content": "Choose **Create** for a new topic, a newly enabled system-assigned identity, or when that identity lacks **Storage Queue Data Message Sender** on the storage account. **Use existing** skips that grant; it does not discover or repair permissions. The collector's blob and queue grants are still deployed.\n\nIf Connect fails with **Managed Identity Authorization Error**, wait about five minutes and retry with the same alias, topic, and other values. RBAC propagation can take up to ten minutes. Do not recreate the topic or toggle its identity, as that changes the principal.\n\nFor storage in an Enforced Network Security Perimeter, separately allow Event Grid's subscription and the collector's service traffic as described in [Enable network security](https://learn.microsoft.com/azure/sentinel/enable-storage-network-security). This template does not change networking." + } + } + ] + } + ] + } } - ], - "innerSteps": null + ] } ], "isConnectivityCriteriasMatchSome": false @@ -419,6 +501,21 @@ "defaultValue": "principalId", "type": "string" }, + "friendlyName": { + "type": "string", + "minLength": 1, + "metadata": { + "description": "Unique connection alias within this workspace. Reuse the same alias when retrying." + } + }, + "eventGridRbacMode": { + "defaultValue": "CreateRoleAssignment", + "type": "string", + "allowedValues": [ + "CreateRoleAssignment", + "UseExistingRoleAssignment" + ] + }, "blobContainerUri": { "defaultValue": "blobContainerUri", "type": "string" @@ -446,39 +543,33 @@ }, "variables": { "_dataConnectorContentIdConnections": "[variables('_dataConnectorContentIdConnections')]", + "_workspaceResourceId": "[variables('workspaceResourceId')]", "connectorName": "storageblob", + "connectionSuffix": "[[uniqueString(toLower(variables('_workspaceResourceId')), parameters('connectorDefinitionName'), parameters('friendlyName'))]", + "connectorResourceName": "[[concat('CustomAzureStorageBlob', variables('connectionSuffix'))]", "storageAccountName": "[[split(split(parameters('blobContainerUri'), 'https://')[1], '.blob.core.windows.net')[0]]", "blobContainerName": "[[split(split(parameters('blobContainerUri'), '.blob.core.windows.net/')[1], '/')[0]]", - "queueName": "[[concat(variables('connectorName'), '-notification')]", - "dlqName": "[[concat(variables('connectorName'), '-dlq')]", + "queueName": "[[concat(variables('connectorName'), '-notification-', variables('connectionSuffix'))]", + "dlqName": "[[concat(variables('connectorName'), '-dlq-', variables('connectionSuffix'))]", "ResourcesIdPrefix": "[[format('/subscriptions/{0}/resourceGroups/{1}/providers', parameters('StorageAccountSubscription'), parameters('StorageAccountResourceGroupName'))]", "storageAccountId": "[[format('{0}/Microsoft.Storage/storageAccounts/{1}', variables('ResourcesIdPrefix'), variables('storageAccountName'))]", - "blobContainerResourceId": "[[format('{0}/blobServices/default/containers/{1}', variables('storageAccountId'), variables('blobContainerName'))]", - "notificationQueueResourceId": "[[format('{0}/queueServices/default/queues/{1}', variables('storageAccountId'), variables('queueName'))]", - "dlqResourceId": "[[format('{0}/queueServices/default/queues/{1}', variables('storageAccountId'), variables('dlqName'))]", - "EGSystemTopicDefaultName": "[[format('eg-system-topic-{0}-{1}', variables('connectorName'), parameters('workspaceName'))]", + "EGSystemTopicDefaultName": "[[format('eg-system-topic-{0}-{1}', variables('connectorName'), uniqueString(toLower(variables('storageAccountId'))))]", "EGSystemTopicName": "[[if(empty(parameters('EGSystemTopicName')), variables('EGSystemTopicDefaultName'), parameters('EGSystemTopicName'))]", "EGTopicResourceId": "[[format('{0}/Microsoft.EventGrid/systemTopics/{1}', variables('ResourcesIdPrefix'), variables('EGSystemTopicName'))]", - "EgSubscriptionName": "[[format('{0}-{1}', variables('connectorName'), 'blobcreatedevents')]", - "EgSubscriptionResourceId": "[[format('{0}/Microsoft.EventGrid/systemTopics/{1}/eventSubscriptions/{2}', variables('ResourcesIdPrefix'), variables('EGSystemTopicName'), variables('EgSubscriptionName'))]", - "storageBlobContributorRoleId": "[[format('/subscriptions/{0}/providers/Microsoft.Authorization/roleDefinitions/ba92f5b4-2d11-453d-a403-e96b0029c9fe', parameters('StorageAccountSubscription'))]", - "storageQueueContributorRoleId": "[[format('/subscriptions/{0}/providers/Microsoft.Authorization/roleDefinitions/974c5e8b-45b9-4653-ba55-5f855dd0fb88', parameters('StorageAccountSubscription'))]", - "blobRaGuid": "[[guid(toLower(variables('blobContainerResourceId')), toLower(parameters('principalId')), toLower(variables('storageBlobContributorRoleId')))]", - "notificationQueueRaGuid": "[[guid(toLower(variables('notificationQueueResourceId')), toLower(parameters('principalId')), toLower(variables('storageQueueContributorRoleId')))]", - "dlqRaGuid": "[[guid(toLower(variables('dlqResourceId')), toLower(parameters('principalId')), toLower(variables('storageQueueContributorRoleId')))]", - "blobRoleAssignmentResourceId": "[[format('{0}/providers/Microsoft.Authorization/roleAssignments/{1}', variables('blobContainerResourceId'), variables('blobRaGuid'))]", - "notificationQueueRoleAssignmentResourceId": "[[format('{0}/providers/Microsoft.Authorization/roleAssignments/{1}', variables('notificationQueueResourceId'), variables('notificationQueueRaGuid'))]", - "dlqRoleAssignmentResourceId": "[[format('{0}/providers/Microsoft.Authorization/roleAssignments/{1}', variables('dlqResourceId'), variables('dlqRaGuid'))]", - "nestedDeploymentName": "CreateDataFlowResources", + "EgSubscriptionName": "[[format('{0}-blobcreatedevents-{1}', variables('connectorName'), variables('connectionSuffix'))]", + "nestedDeploymentName": "[[concat('CreateDataFlowResources-', variables('connectionSuffix'))]", "nestedDeploymentId": "[[format('{0}/Microsoft.Resources/deployments/{1}', variables('ResourcesIdPrefix'), variables('nestedDeploymentName'))]" }, "resources": [ { - "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('DataConnector-', variables('_dataConnectorContentIdConnections')))]", + "name": "[[concat(parameters('workspaceName'), '/Microsoft.SecurityInsights/DataConnector-', variables('connectorResourceName'))]", "apiVersion": "2022-01-01-preview", "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "dependsOn": [ + "[[extensionResourceId(variables('_workspaceResourceId'), 'Microsoft.SecurityInsights/dataConnectors', variables('connectorResourceName'))]" + ], "properties": { - "parentId": "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/dataConnectors', variables('_dataConnectorContentIdConnections'))]", + "parentId": "[[extensionResourceId(variables('_workspaceResourceId'), 'Microsoft.SecurityInsights/dataConnectors', variables('connectorResourceName'))]", "contentId": "[variables('_dataConnectorContentIdConnections')]", "kind": "ResourcesDataConnector", "version": "[variables('dataConnectorVersionConnections')]", @@ -498,13 +589,137 @@ }, { "type": "Microsoft.Resources/deployments", - "apiVersion": "2021-04-01", + "apiVersion": "2025-04-01", "name": "[[variables('nestedDeploymentName')]", "properties": { "mode": "Incremental", + "expressionEvaluationOptions": { + "scope": "inner" + }, + "parameters": { + "StorageAccountLocation": { + "value": "[[parameters('StorageAccountLocation')]" + }, + "blobFolderName": { + "value": "[[parameters('blobFolderName')]" + }, + "principalId": { + "value": "[[parameters('principalId')]" + }, + "eventGridRbacMode": { + "value": "[[parameters('eventGridRbacMode')]" + }, + "existingSystemTopic": { + "value": "[[if(empty(parameters('EGSystemTopicName')), createObject(), reference(variables('EGTopicResourceId'), '2025-02-15', 'Full'))]" + }, + "existingSystemTopicMatchesStorageAccount": { + "value": "[[if(empty(parameters('EGSystemTopicName')), true(), equals(toLower(reference(variables('EGTopicResourceId'), '2025-02-15', 'Full').properties.source), toLower(variables('storageAccountId'))))]" + }, + "storageAccountName": { + "value": "[[variables('storageAccountName')]" + }, + "storageAccountId": { + "value": "[[variables('storageAccountId')]" + }, + "blobContainerName": { + "value": "[[variables('blobContainerName')]" + }, + "queueName": { + "value": "[[variables('queueName')]" + }, + "dlqName": { + "value": "[[variables('dlqName')]" + }, + "EGSystemTopicName": { + "value": "[[variables('EGSystemTopicName')]" + }, + "EGTopicResourceId": { + "value": "[[variables('EGTopicResourceId')]" + }, + "EgSubscriptionName": { + "value": "[[variables('EgSubscriptionName')]" + } + }, "template": { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", + "parameters": { + "StorageAccountLocation": { + "type": "string" + }, + "blobFolderName": { + "type": "string", + "defaultValue": "" + }, + "principalId": { + "type": "string" + }, + "eventGridRbacMode": { + "type": "string", + "allowedValues": [ + "CreateRoleAssignment", + "UseExistingRoleAssignment" + ] + }, + "existingSystemTopic": { + "type": "object" + }, + "existingSystemTopicMatchesStorageAccount": { + "type": "bool", + "allowedValues": [ + true + ], + "metadata": { + "description": "The existing Event Grid system topic must use the selected storage account as its source." + } + }, + "storageAccountName": { + "type": "string" + }, + "storageAccountId": { + "type": "string" + }, + "blobContainerName": { + "type": "string" + }, + "queueName": { + "type": "string" + }, + "dlqName": { + "type": "string" + }, + "EGSystemTopicName": { + "type": "string" + }, + "EGTopicResourceId": { + "type": "string" + }, + "EgSubscriptionName": { + "type": "string" + } + }, + "variables": { + "storageAccountName": "[[parameters('storageAccountName')]", + "storageAccountId": "[[parameters('storageAccountId')]", + "blobContainerName": "[[parameters('blobContainerName')]", + "queueName": "[[parameters('queueName')]", + "dlqName": "[[parameters('dlqName')]", + "EGSystemTopicName": "[[parameters('EGSystemTopicName')]", + "EGTopicResourceId": "[[parameters('EGTopicResourceId')]", + "EgSubscriptionName": "[[parameters('EgSubscriptionName')]", + "blobContainerResourceId": "[[format('{0}/blobServices/default/containers/{1}', variables('storageAccountId'), variables('blobContainerName'))]", + "notificationQueueResourceId": "[[resourceId('Microsoft.Storage/storageAccounts/queueServices/queues', variables('storageAccountName'), 'default', variables('queueName'))]", + "dlqResourceId": "[[resourceId('Microsoft.Storage/storageAccounts/queueServices/queues', variables('storageAccountName'), 'default', variables('dlqName'))]", + "storageBlobContributorRoleId": "[[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')]", + "storageQueueContributorRoleId": "[[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88')]", + "blobRaGuid": "[[guid(toLower(variables('blobContainerResourceId')), toLower(parameters('principalId')), toLower(variables('storageBlobContributorRoleId')))]", + "notificationQueueRaGuid": "[[guid(toLower(variables('notificationQueueResourceId')), toLower(parameters('principalId')), toLower(variables('storageQueueContributorRoleId')))]", + "dlqRaGuid": "[[guid(toLower(variables('dlqResourceId')), toLower(parameters('principalId')), toLower(variables('storageQueueContributorRoleId')))]", + "eventGridTopicDeploymentName": "[[concat('PrepareEventGridTopic-', uniqueString(variables('EGTopicResourceId'), variables('queueName')))]", + "eventGridTopicDeploymentId": "[[resourceId('Microsoft.Resources/deployments', variables('eventGridTopicDeploymentName'))]", + "eventGridQueueSenderDeploymentName": "[[concat('EventGridQueueSender-', uniqueString(variables('EGTopicResourceId'), variables('queueName')))]", + "eventGridQueueSenderDeploymentId": "[[resourceId('Microsoft.Resources/deployments', variables('eventGridQueueSenderDeploymentName'))]" + }, "resources": [ { "type": "Microsoft.Storage/storageAccounts/queueServices/queues", @@ -521,40 +736,154 @@ "properties": {} }, { - "type": "Microsoft.EventGrid/systemTopics", - "apiVersion": "2022-06-15", - "name": "[[variables('EGSystemTopicName')]", - "location": "[[parameters('StorageAccountLocation')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[[variables('eventGridTopicDeploymentName')]", + "condition": "[[parameters('existingSystemTopicMatchesStorageAccount')]", "properties": { - "provisioningState": "Succeeded", - "source": "[[variables('storageAccountId')]", - "topicType": "microsoft.storage.storageaccounts", - "metricResourceId": "0cefe8d9-3269-4f68-a44e-46a4fc26e4a9" - }, - "condition": "[[empty(parameters('EGSystemTopicName'))]" + "mode": "Incremental", + "expressionEvaluationOptions": { + "scope": "inner" + }, + "parameters": { + "topicName": { + "value": "[[variables('EGSystemTopicName')]" + }, + "StorageAccountLocation": { + "value": "[[parameters('StorageAccountLocation')]" + }, + "sourceId": { + "value": "[[variables('storageAccountId')]" + }, + "existingSystemTopic": { + "value": "[[parameters('existingSystemTopic')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "topicName": { + "type": "string" + }, + "StorageAccountLocation": { + "type": "string" + }, + "sourceId": { + "type": "string" + }, + "existingSystemTopic": { + "type": "object" + } + }, + "variables": { + "existingTopicIdentity": "[[if(contains(parameters('existingSystemTopic'), 'identity'), coalesce(parameters('existingSystemTopic').identity, createObject()), createObject())]", + "existingUserAssignedIdentities": "[[if(contains(variables('existingTopicIdentity'), 'userAssignedIdentities'), coalesce(variables('existingTopicIdentity').userAssignedIdentities, createObject()), createObject())]", + "topicIdentity": "[[if(empty(variables('existingUserAssignedIdentities')), createObject('type', 'SystemAssigned'), createObject('type', 'SystemAssigned, UserAssigned', 'userAssignedIdentities', variables('existingUserAssignedIdentities')))]" + }, + "resources": [ + { + "type": "Microsoft.EventGrid/systemTopics", + "apiVersion": "2025-02-15", + "name": "[[parameters('topicName')]", + "location": "[[if(empty(parameters('existingSystemTopic')), parameters('StorageAccountLocation'), parameters('existingSystemTopic').location)]", + "properties": { + "source": "[[if(empty(parameters('existingSystemTopic')), parameters('sourceId'), parameters('existingSystemTopic').properties.source)]", + "topicType": "[[if(empty(parameters('existingSystemTopic')), 'microsoft.storage.storageaccounts', parameters('existingSystemTopic').properties.topicType)]" + }, + "identity": "[[variables('topicIdentity')]", + "tags": "[[if(contains(parameters('existingSystemTopic'), 'tags'), coalesce(parameters('existingSystemTopic').tags, createObject()), createObject())]" + } + ], + "outputs": { + "principalId": { + "type": "string", + "value": "[[reference(resourceId('Microsoft.EventGrid/systemTopics', parameters('topicName')), '2025-02-15', 'Full').identity.principalId]" + } + } + } + } }, { - "type": "Microsoft.EventGrid/systemTopics/eventSubscriptions", - "apiVersion": "2023-12-15-preview", - "name": "[[format('{0}/{1}', variables('EGSystemTopicName'), variables('EgSubscriptionName'))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[[variables('eventGridQueueSenderDeploymentName')]", + "condition": "[[equals(parameters('eventGridRbacMode'), 'CreateRoleAssignment')]", "dependsOn": [ - "[[variables('EGTopicResourceId')]", + "[[variables('eventGridTopicDeploymentId')]", "[[variables('notificationQueueResourceId')]" ], "properties": { - "destination": { - "endpointType": "StorageQueue", - "properties": { - "queueName": "[[variables('queueName')]", - "resourceId": "[[variables('storageAccountId')]" + "mode": "Incremental", + "expressionEvaluationOptions": { + "scope": "inner" + }, + "parameters": { + "principalId": { + "value": "[[if(equals(parameters('eventGridRbacMode'), 'CreateRoleAssignment'), reference(variables('eventGridTopicDeploymentId'), '2025-04-01').outputs.principalId.value, '')]" + }, + "storageAccountResourceId": { + "value": "[[variables('storageAccountId')]" } }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "principalId": { + "type": "string" + }, + "storageAccountResourceId": { + "type": "string" + } + }, + "variables": { + "queueMessageSenderRoleId": "[[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'c6a89b2d-59bc-44d0-9896-0f6e12d7b80a')]" + }, + "resources": [ + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "name": "[[guid(toLower(parameters('storageAccountResourceId')), toLower(parameters('principalId')), toLower(variables('queueMessageSenderRoleId')))]", + "scope": "[[parameters('storageAccountResourceId')]", + "properties": { + "roleDefinitionId": "[[variables('queueMessageSenderRoleId')]", + "principalId": "[[parameters('principalId')]", + "principalType": "ServicePrincipal" + } + } + ] + } + } + }, + { + "type": "Microsoft.EventGrid/systemTopics/eventSubscriptions", + "apiVersion": "2025-02-15", + "name": "[[format('{0}/{1}', variables('EGSystemTopicName'), variables('EgSubscriptionName'))]", + "dependsOn": [ + "[[if(equals(parameters('eventGridRbacMode'), 'CreateRoleAssignment'), variables('eventGridQueueSenderDeploymentId'), variables('eventGridTopicDeploymentId'))]", + "[[variables('notificationQueueResourceId')]" + ], + "properties": { "filter": { "includedEventTypes": [ "Microsoft.Storage.BlobCreated" ], "subjectBeginsWith": "[[format('{0}/{1}/blobs/{2}', '/blobServices/default/containers', variables('blobContainerName'), if(empty(parameters('blobFolderName')), '', concat(parameters('blobFolderName'),'/')))]" - } + }, + "deliveryWithResourceIdentity": { + "identity": { + "type": "SystemAssigned" + }, + "destination": { + "endpointType": "StorageQueue", + "properties": { + "queueName": "[[variables('queueName')]", + "resourceId": "[[variables('storageAccountId')]" + } + } + }, + "eventDeliverySchema": "EventGridSchema" } }, { @@ -603,13 +932,14 @@ "resourceGroup": "[[parameters('StorageAccountResourceGroupName')]" }, { - "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/', 'CustomAzureStorageBlob')]", + "name": "[[concat(parameters('workspaceName'), '/Microsoft.SecurityInsights/', variables('connectorResourceName'))]", "apiVersion": "2022-12-01-preview", "type": "Microsoft.OperationalInsights/workspaces/providers/dataConnectors", "location": "[parameters('workspace-location')]", "kind": "StorageAccountBlobContainer", "properties": { "connectorDefinitionName": "[[parameters('connectorDefinitionName')]", + "dataType": "[variables('_logAnalyticsTableId1')]", "dcrConfig": { "streamName": "Custom-StorageBlob // Modify your stream name (same as 143)", "dataCollectionEndpoint": "[[parameters('dcrConfig').dataCollectionEndpoint]", @@ -620,7 +950,9 @@ }, "request": { "QueueUri": "[[concat('https://', variables('storageAccountName'), '.queue.core.windows.net/', variables('queueName'))]", - "DlqUri": "[[concat('https://', variables('storageAccountName'), '.queue.core.windows.net/', variables('dlqName'))]" + "DlqUri": "[[concat('https://', variables('storageAccountName'), '.queue.core.windows.net/', variables('dlqName'))]", + "StorageAccountSubscriptionId": "[[parameters('StorageAccountSubscription')]", + "StorageAccountResourceGroupName": "[[parameters('StorageAccountResourceGroupName')]" }, "response": { "eventsJsonPaths": [ @@ -628,9 +960,14 @@ ], "format": "json", "isGzipCompressed": false + }, + "addOnAttributes": { + "friendlyName": "[[parameters('friendlyName')]" } }, - "dependsOn": [] + "dependsOn": [ + "[[variables('nestedDeploymentId')]" + ] } ] }, diff --git a/DataConnectors/Templates/Data Connectors Template Guidance.md b/DataConnectors/Templates/Data Connectors Template Guidance.md index 8633afdc987..a50dab34109 100644 --- a/DataConnectors/Templates/Data Connectors Template Guidance.md +++ b/DataConnectors/Templates/Data Connectors Template Guidance.md @@ -101,6 +101,82 @@ A data connector can have multiple data types and these can be represented by co Expand and add multiple instructions as needed by adding more title and description elements in this block. +## Storage Blob CCF V2 template + +Use `Connector_StorageBlob_CCF_templateV2.json` for Storage Blob connectors that +authenticate with the first-party application using `ManagedServicePrincipal`. +Keep the first-party application ID in the service-principal control; the +tenant-local principal selected by that control receives the collector's blob +and queue role assignments. Replace the solution, table, stream, and other +authoring placeholders before deploying. + +### Multiple connections + +The connector page uses `DataConnectorsGrid` to display +`properties.addOnAttributes.friendlyName`, with `DeleteConnector` as the row +action. An **Add Connection** `ContextPane` of type `DataConnectorsContextPane` +contains the connection form in its own `instructionSteps`; it replaces the +single-connection toggle. + +The required `friendlyName` parameter is the connection alias. The workspace +resource ID, connector definition, and alias determine a stable suffix used for +the connector, metadata, notification/dead-letter queues, event subscription, +and data-flow deployment. Use a different alias for each connection and keep it +unchanged on retries. This isolates connections even when they use the same +storage account or different containers/folders within it. Overlapping source +filters can cause duplicate ingestion. + +The connector request includes `StorageAccountSubscriptionId` and +`StorageAccountResourceGroupName` as well as its queue URLs, allowing the storage +account to be in a different subscription or resource group from the workspace. +The connector waits for the storage-side deployment to finish before creation. + +### Managed-identity Event Grid delivery + +The Event Grid system topic has a system-assigned identity. When supplying an +existing topic name, the deployment reads the topic, validates that its source +is the selected storage account, and preserves its location, source, topic +type, tags, attached user-assigned identities, and existing system-assigned +principal while enabling system-assigned identity. + +There can be only one system topic per storage account. If a topic already +exists, including one created by an earlier connection, supply its exact name. +Leave the field empty only to create a topic; the generated topic name is +stable per storage account. Connections share the topic but have separate +event subscriptions and queues. + +The `eventGridRbacMode` parameter supports: + +- `CreateRoleAssignment` (default): grant **Storage Queue Data Message Sender** + (`c6a89b2d-59bc-44d0-9896-0f6e12d7b80a`) to the topic's system-assigned + identity at **storage-account scope**, then create the event subscription. +- `UseExistingRoleAssignment`: skip creating that sender grant. Use this only + when the same system-assigned principal already has the required + storage-account permission; permissions are not discovered or repaired. + +Both modes configure `deliveryWithResourceIdentity` with `SystemAssigned` and +`EventGridSchema`. The event subscription waits for topic preparation and, in +Create mode, the sender-role deployment. It retains the container/folder +`Microsoft.Storage.BlobCreated` filter. + +The Event Grid identity only sends queue messages. The separate first-party +collector retains **Storage Blob Data Contributor** on the selected container +and **Storage Queue Data Contributor** on each connection's queues. Collector +role-assignment names remain derived from normalized scope, tenant-local +principal ID, and role-definition ID. Deployments remain incremental; changing +an alias creates new connection resources rather than renaming or removing +previous ones. + +The deployer needs resource deployment permissions and role-assignment write +permissions at the relevant storage scope. Register `Microsoft.EventGrid` in +the storage subscription. RBAC propagation may take up to ten minutes: for a +managed-identity authorization error, wait and retry with the same values +instead of recreating the topic or its identity. + +This template does not change storage networking or Network Security Perimeter +configuration. Configure any required network access separately; see +[Enable network security](https://learn.microsoft.com/azure/sentinel/enable-storage-network-security). + ## Next steps Currently in preview, you can also publish your data connector as a Microsoft Sentinel solution. From e6dc943eb71a183a8a4e746ae07010e752ee82d3 Mon Sep 17 00:00:00 2001 From: Alex Li Date: Mon, 26 Oct 2026 15:46:12 -0500 Subject: [PATCH 5/6] Fix V3 Azure Storage connector packaging Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../V3/CCF_README.md | 16 +- .../common/createCCPConnector.ps1 | 2 +- ...tGridManagedIdentityDeploymentTemplate.ps1 | 35 ++-- .../storageAccountDeploymentTemplate.ps1 | 96 ++++++--- .../tests/EventGridManagedIdentity.Tests.ps1 | 194 ++++++++++++++++-- 5 files changed, 276 insertions(+), 67 deletions(-) diff --git a/Tools/Create-Azure-Sentinel-Solution/V3/CCF_README.md b/Tools/Create-Azure-Sentinel-Solution/V3/CCF_README.md index f6984325361..545d6aa1255 100644 --- a/Tools/Create-Azure-Sentinel-Solution/V3/CCF_README.md +++ b/Tools/Create-Azure-Sentinel-Solution/V3/CCF_README.md @@ -169,9 +169,21 @@ Table discovery excludes a connector's already assigned primary table from the a Set `"eventGridUseManagedIdentity": true` on a `StorageAccountBlobContainer` poller to use the system topic's system-assigned identity for queue delivery. This generator-only boolean is removed from the emitted data connector. GitHub enables it; omitted or false values preserve the existing delivery template for other connectors. -In this mode, ARM creates or updates the system topic with a system-assigned identity. The existing-topic reference is evaluated outside the inner-scoped topic deployment so the previous identity and tags are available before the update. Topics with attached user-assigned identities retain them through `SystemAssigned, UserAssigned`; otherwise, the identity type is `SystemAssigned`. The post-deployment principal ID is used for sender access. The `eventGridRbacMode` parameter defaults to `CreateRoleAssignment`; `UseExistingRoleAssignment` skips the Event Grid sender-role deployment and relies on a pre-existing grant. Use Create when system-assigned identity is being enabled for the first time. GitHub exposes these mutually exclusive choices through the connector UI's existing `Radio` control support. Other opt-in connector definitions can use the same parameter and values. +In this mode, ARM creates or updates the system topic with a system-assigned identity. The existing-topic reference is evaluated outside the inner-scoped storage deployment so the previous identity and tags are available before the update. The storage deployment rejects a supplied topic whose source does not match the selected storage account, comparing resource IDs case-insensitively. Existing topics retain their location, source, topic type, tags, and attached identities. Topics with attached user-assigned identities retain them through `SystemAssigned, UserAssigned`; otherwise, the identity type is `SystemAssigned`. The post-deployment principal ID is used for sender access. The `eventGridRbacMode` parameter defaults to `CreateRoleAssignment`; `UseExistingRoleAssignment` skips the Event Grid sender-role deployment and relies on a pre-existing grant. The principal-output reference is guarded so it is not evaluated for a skipped sender deployment. Use Create when system-assigned identity is being enabled for the first time. GitHub exposes these mutually exclusive choices through the connector UI's existing `Radio` control support. Other opt-in connector definitions can use the same parameter and values. -When Create is selected, the resolved topic principal is passed to a separate nested deployment so the **Storage Queue Data Message Sender** assignment retains its deterministic name based on destination storage-account scope, principal, and role. Use account scope as specified by [Event Grid Storage Queue delivery](https://learn.microsoft.com/azure/event-grid/managed-service-identity#use-the-azure-cli---azure-storage-queue). This grants message-add access to queues in the selected account, not message-read/delete or blob access. The generated event subscription is always deployed with `deliveryWithResourceIdentity` using `SystemAssigned`; it depends on the sender deployment for Create or the topic-preparation deployment for Use existing, and always depends on the notification queue. Subscription delivery settings and filters remain connector-managed. The three Sentinel service-principal grants and their legacy naming policy remain unchanged. No deployment scripts, user-assigned identity resources, temporary storage, or container instances are required. +When Create is selected, the resolved topic principal is passed to a separate nested deployment so the **Storage Queue Data Message Sender** assignment retains its deterministic name based on destination storage-account scope, principal, and role. Use account scope as specified by [Event Grid Storage Queue delivery](https://learn.microsoft.com/azure/event-grid/managed-service-identity#use-the-azure-cli---azure-storage-queue). This grants message-add access to queues in the selected account, not message-read/delete or blob access. The generated event subscription is always deployed with `deliveryWithResourceIdentity` using `SystemAssigned`; it depends on the sender deployment for Create or the topic-preparation deployment for Use existing, and always depends on the notification queue. Subscription delivery settings and filters remain connector-managed. No deployment scripts, user-assigned identity resources, temporary storage, or container instances are required. + +The optional `blobFolderName` parameter defaults to an empty string. Managed-identity delivery filters on `/blobServices/default/containers/{container}/blobs/` when it is empty, or that prefix followed by `{folder}/` when supplied. `eventGridAdvancedFilters`, such as GitHub's `/_check` exclusion, are preserved. Storage-account and topic IDs passed into the inner-scoped deployment explicitly use `StorageAccountSubscription` and `StorageAccountResourceGroupName`, so storage can be in a different subscription or resource group from the workspace. + +Collector authentication is separate from Event Grid delivery. A poller with `"properties": { "auth": { "type": "ManagedServicePrincipal" } }` retains that authentication type, uses the connection's `connectorDefinitionName` parameter, and does not generate the three legacy Sentinel collector role assignments, regardless of `eventGridUseManagedIdentity`. Grant the managed collector **Storage Blob Data Reader** and **Storage Queue Data Contributor** on the storage account before connecting, as described by its connector definition. Pollers using `ServicePrincipal` retain the three existing collector grants and their naming policy. Changing Event Grid delivery alone does not migrate collector authentication or remove existing grants. + +Run the offline packaging regressions with: + +```powershell +pwsh -NoProfile -File Tools\Create-Azure-Sentinel-Solution\tests\EventGridManagedIdentity.Tests.ps1 +``` + +For a generated GitHub connector, optionally pass `-ConnectorDirectory "" -ReferenceTemplatePath ""`. The tests exercise CCF discovery, parameter merging, source UI/DCR preservation, both collector authentication modes, V3-depth serialization, and structural equality of the managed-collector storage deployment with the supplied reference. They create and remove an isolated temporary fixture; no solution version is changed and nothing is deployed to Azure. These checks do not evaluate ARM expressions or prove runtime RBAC propagation. Upgrades from the earlier queue-scoped sender grant create a distinct account-scoped assignment; the deterministic naming formula remains scope/principal/role-based. Incremental deployment leaves the previous queue grant in place and does not modify its scope or reuse its GUID. Newly created grants can still require up to 10 minutes for RBAC propagation; ARM dependencies ensure creation order, not data-plane readiness. Retry with the same topic and identity rather than recreating them. diff --git a/Tools/Create-Azure-Sentinel-Solution/common/createCCPConnector.ps1 b/Tools/Create-Azure-Sentinel-Solution/common/createCCPConnector.ps1 index 2f8dcba97a2..6ca24dd5810 100644 --- a/Tools/Create-Azure-Sentinel-Solution/common/createCCPConnector.ps1 +++ b/Tools/Create-Azure-Sentinel-Solution/common/createCCPConnector.ps1 @@ -770,7 +770,7 @@ function createCCPConnectorResources($contentResourceDetails, $dataFileMetadata, } elseif ($armResource.kind.ToLower() -eq 'storageaccountblobcontainer') { . "$PSScriptRoot/storageAccountDeploymentTemplate.ps1" # load storage resource creator - CreateStorageAccountBlobContainerResourceProperties -armResource $armResource -templateContentConnections $templateContentConnections -fileType $fileType + CreateStorageAccountBlobContainerResourceProperties -armResource $armResource -templateContentConnections $templateContentConnections -fileType $fileType -instructionParameters $paramItems } elseif ($armResource.kind.ToLower() -eq 'oci') { CreateOciResourceProperties -armResource $armResource -templateContentConnections $templateContentConnections -fileType $fileType diff --git a/Tools/Create-Azure-Sentinel-Solution/common/eventGridManagedIdentityDeploymentTemplate.ps1 b/Tools/Create-Azure-Sentinel-Solution/common/eventGridManagedIdentityDeploymentTemplate.ps1 index ccf0edbafd0..66ac12137c9 100644 --- a/Tools/Create-Azure-Sentinel-Solution/common/eventGridManagedIdentityDeploymentTemplate.ps1 +++ b/Tools/Create-Azure-Sentinel-Solution/common/eventGridManagedIdentityDeploymentTemplate.ps1 @@ -2,8 +2,7 @@ function Get-EventGridRbacModeParameter { param([switch]$IncludeDefault) $parameter = [PSCustomObject]@{ - type = 'securestring' - minLength = 1 + type = 'string' allowedValues = @('CreateRoleAssignment', 'UseExistingRoleAssignment') } if ($IncludeDefault) { @@ -18,6 +17,7 @@ function Get-EventGridManagedIdentityDeployments { type = 'Microsoft.Resources/deployments' apiVersion = '2025-04-01' name = "[[variables('eventGridTopicDeploymentName')]" + condition = "[[parameters('existingSystemTopicMatchesStorageAccount')]" properties = [ordered]@{ mode = 'Incremental' expressionEvaluationOptions = @{ scope = 'inner' } @@ -25,11 +25,10 @@ function Get-EventGridManagedIdentityDeployments { topicName = @{ value = "[[variables('EGSystemTopicName')]" } StorageAccountLocation = @{ value = "[[parameters('StorageAccountLocation')]" } sourceId = @{ - value = "[[resourceId(parameters('StorageAccountSubscription'), parameters('StorageAccountResourceGroupName'), 'Microsoft.Storage/storageAccounts', variables('storageAccountName'))]" + value = "[[variables('storageAccountId')]" } - # Read the supplied topic before updating it in the inner-scoped deployment. - existingTopic = @{ - value = "[[if(empty(parameters('EGSystemTopicName')), createObject(), reference(resourceId(parameters('StorageAccountSubscription'), parameters('StorageAccountResourceGroupName'), 'Microsoft.EventGrid/systemTopics', variables('EGSystemTopicName')), '2025-02-15', 'Full'))]" + existingSystemTopic = @{ + value = "[[parameters('existingSystemTopic')]" } } template = [ordered]@{ @@ -37,26 +36,26 @@ function Get-EventGridManagedIdentityDeployments { contentVersion = '1.0.0.0' parameters = [ordered]@{ topicName = @{ type = 'string' } - StorageAccountLocation = @{ type = 'securestring' } + StorageAccountLocation = @{ type = 'string' } sourceId = @{ type = 'string' } - existingTopic = @{ type = 'object' } + existingSystemTopic = @{ type = 'object' } } variables = [ordered]@{ - existingIdentity = "[[if(contains(parameters('existingTopic'), 'identity'), coalesce(parameters('existingTopic').identity, createObject()), createObject())]" - userAssignedIdentities = "[[if(contains(variables('existingIdentity'), 'userAssignedIdentities'), coalesce(variables('existingIdentity').userAssignedIdentities, createObject()), createObject())]" - topicIdentity = "[[if(empty(variables('userAssignedIdentities')), createObject('type', 'SystemAssigned'), createObject('type', 'SystemAssigned, UserAssigned', 'userAssignedIdentities', variables('userAssignedIdentities')))]" + existingTopicIdentity = "[[if(contains(parameters('existingSystemTopic'), 'identity'), coalesce(parameters('existingSystemTopic').identity, createObject()), createObject())]" + existingUserAssignedIdentities = "[[if(contains(variables('existingTopicIdentity'), 'userAssignedIdentities'), coalesce(variables('existingTopicIdentity').userAssignedIdentities, createObject()), createObject())]" + topicIdentity = "[[if(empty(variables('existingUserAssignedIdentities')), createObject('type', 'SystemAssigned'), createObject('type', 'SystemAssigned, UserAssigned', 'userAssignedIdentities', variables('existingUserAssignedIdentities')))]" } resources = @( [ordered]@{ type = 'Microsoft.EventGrid/systemTopics' apiVersion = '2025-02-15' name = "[[parameters('topicName')]" - location = "[[parameters('StorageAccountLocation')]" - tags = "[[if(contains(parameters('existingTopic'), 'tags'), coalesce(parameters('existingTopic').tags, createObject()), createObject())]" + location = "[[if(empty(parameters('existingSystemTopic')), parameters('StorageAccountLocation'), parameters('existingSystemTopic').location)]" + tags = "[[if(contains(parameters('existingSystemTopic'), 'tags'), coalesce(parameters('existingSystemTopic').tags, createObject()), createObject())]" identity = "[[variables('topicIdentity')]" properties = [ordered]@{ - source = "[[parameters('sourceId')]" - topicType = 'microsoft.storage.storageaccounts' + source = "[[if(empty(parameters('existingSystemTopic')), parameters('sourceId'), parameters('existingSystemTopic').properties.source)]" + topicType = "[[if(empty(parameters('existingSystemTopic')), 'microsoft.storage.storageaccounts', parameters('existingSystemTopic').properties.topicType)]" } } ) @@ -76,17 +75,17 @@ function Get-EventGridManagedIdentityDeployments { condition = "[[equals(parameters('eventGridRbacMode'), 'CreateRoleAssignment')]" dependsOn = @( "[[variables('eventGridTopicDeploymentId')]", - "[[resourceId(parameters('StorageAccountSubscription'), parameters('StorageAccountResourceGroupName'), 'Microsoft.Storage/storageAccounts/queueServices/queues', variables('storageAccountName'), 'default', variables('queueName'))]" + "[[variables('notificationQueueResourceId')]" ) properties = [ordered]@{ mode = 'Incremental' expressionEvaluationOptions = @{ scope = 'inner' } parameters = [ordered]@{ principalId = @{ - value = "[[reference(variables('eventGridTopicDeploymentId'), '2025-04-01').outputs.principalId.value]" + value = "[[if(equals(parameters('eventGridRbacMode'), 'CreateRoleAssignment'), reference(variables('eventGridTopicDeploymentId'), '2025-04-01').outputs.principalId.value, '')]" } storageAccountResourceId = @{ - value = "[[resourceId(parameters('StorageAccountSubscription'), parameters('StorageAccountResourceGroupName'), 'Microsoft.Storage/storageAccounts', variables('storageAccountName'))]" + value = "[[variables('storageAccountId')]" } } template = [ordered]@{ diff --git a/Tools/Create-Azure-Sentinel-Solution/common/storageAccountDeploymentTemplate.ps1 b/Tools/Create-Azure-Sentinel-Solution/common/storageAccountDeploymentTemplate.ps1 index f83732d76c3..944684f8478 100644 --- a/Tools/Create-Azure-Sentinel-Solution/common/storageAccountDeploymentTemplate.ps1 +++ b/Tools/Create-Azure-Sentinel-Solution/common/storageAccountDeploymentTemplate.ps1 @@ -33,7 +33,8 @@ function Set-ArmVariable { } } -function CreateStorageAccountBlobContainerResourceProperties($armResource, $templateContentConnections, $fileType) { +function CreateStorageAccountBlobContainerResourceProperties($armResource, $templateContentConnections, $fileType, $instructionParameters = $null) { + $isManagedServicePrincipal = $armResource.properties.auth.type -eq 'ManagedServicePrincipal' $eventGridUseManagedIdentity = $false if ($armResource.PSObject.Properties.Name -contains 'eventGridUseManagedIdentity') { if ($armResource.eventGridUseManagedIdentity -isnot [bool]) { @@ -60,6 +61,10 @@ function CreateStorageAccountBlobContainerResourceProperties($armResource, $temp ProcessPropertyPlaceholders -armResource $armResource -templateContentConnections $templateContentConnections -isOnlyObjectCheck $true -propertyObject $armResource.properties -propertyName 'dcrConfig' -isInnerObject $false -innerObjectName $null -kindType $kindType -isSecret $true -isRequired $true -fileType $fileType -minLength 3 -isCreateArray $false + if ($isManagedServicePrincipal) { + $armResource.properties.connectorDefinitionName = "[[parameters('connectorDefinitionName')]" + } + $hasDependsOn = [bool]($armResource.PSobject.Properties.name -match "dependsOn") if (!$hasDependsOn) { $armResource | Add-Member -NotePropertyName "dependsOn" -NotePropertyValue @( "[[variables('nestedDeploymentId')]") @@ -76,13 +81,20 @@ function CreateStorageAccountBlobContainerResourceProperties($armResource, $temp Set-ResourceVariables -eventGridUseManagedIdentity $eventGridUseManagedIdentity if ($eventGridUseManagedIdentity) { . (Join-Path $PSScriptRoot 'eventGridManagedIdentityDeploymentTemplate.ps1') - $templateContentConnections.properties.mainTemplate.parameters | - Add-Member -NotePropertyName 'eventGridRbacMode' -NotePropertyValue (Get-EventGridRbacModeParameter -IncludeDefault) -Force + # Keep UI-derived parameters in sync so the final merge cannot restore a required folder. + foreach ($parameterSet in @($templateContentConnections.properties.mainTemplate.parameters, $instructionParameters)) { + if ($null -eq $parameterSet) { continue } + $parameterSet | Add-Member -NotePropertyName 'eventGridRbacMode' -NotePropertyValue (Get-EventGridRbacModeParameter -IncludeDefault) -Force + $parameterSet | Add-Member -NotePropertyName 'blobFolderName' -NotePropertyValue ([PSCustomObject]@{ + type = 'string' + defaultValue = '' + }) -Force + } } - $templateContentConnections.properties.mainTemplate.resources += Get-StorageAccountDeploymentTemplate -eventGridAdvancedFilters $eventGridAdvancedFilters -eventGridUseManagedIdentity $eventGridUseManagedIdentity -resourceVariables $templateContentConnections.properties.mainTemplate.variables + $templateContentConnections.properties.mainTemplate.resources += Get-StorageAccountDeploymentTemplate -eventGridAdvancedFilters $eventGridAdvancedFilters -eventGridUseManagedIdentity $eventGridUseManagedIdentity -resourceVariables $templateContentConnections.properties.mainTemplate.variables -isManagedServicePrincipal $isManagedServicePrincipal } catch { - if ($eventGridUseManagedIdentity) { throw } + if ($eventGridUseManagedIdentity -or $isManagedServicePrincipal) { throw } Write-Host "Error in CreateStorageAccountBlobContainerResourceProperties function. Error Details $_" } } @@ -91,7 +103,8 @@ function Get-StorageAccountDeploymentTemplate { param ( [object]$eventGridAdvancedFilters = $null, [bool]$eventGridUseManagedIdentity = $false, - [System.Collections.IDictionary]$resourceVariables = $null + [System.Collections.IDictionary]$resourceVariables = $null, + [bool]$isManagedServicePrincipal = $false ) $storageDeploymentTemplate = [ordered]@{ @@ -190,6 +203,13 @@ function Get-StorageAccountDeploymentTemplate { resourceGroup = "[[parameters('StorageAccountResourceGroupName')]" } + if ($isManagedServicePrincipal) { + $storageDeploymentTemplate.properties.template.resources = @( + $storageDeploymentTemplate.properties.template.resources | + Where-Object { $_.type -notlike '*/providers/roleAssignments' } + ) + } + $hasAdvancedFilters = $false if ($null -ne $eventGridAdvancedFilters) { if ($eventGridAdvancedFilters -is [System.Array]) { @@ -220,41 +240,62 @@ function Get-StorageAccountDeploymentTemplate { $storageDeploymentTemplate.properties['expressionEvaluationOptions'] = @{ scope = 'inner' } $storageDeploymentTemplate.properties['parameters'] = [ordered]@{} $storageDeploymentTemplate.properties.template['parameters'] = [ordered]@{} - foreach ($parameterName in @( - 'StorageAccountLocation', 'StorageAccountSubscription', 'StorageAccountResourceGroupName', - 'blobContainerUri', 'EGSystemTopicName', 'principalId', 'innerWorkspace' - )) { + foreach ($parameterName in @('StorageAccountLocation', 'blobFolderName', 'eventGridRbacMode')) { $storageDeploymentTemplate.properties.parameters[$parameterName] = @{ value = "[[parameters('$parameterName')]" } - $storageDeploymentTemplate.properties.template.parameters[$parameterName] = @{ type = 'securestring' } - } - $storageDeploymentTemplate.properties.parameters['eventGridRbacMode'] = @{ - value = "[[parameters('eventGridRbacMode')]" + $storageDeploymentTemplate.properties.template.parameters[$parameterName] = @{ type = 'string' } } + $storageDeploymentTemplate.properties.template.parameters['blobFolderName']['defaultValue'] = '' $storageDeploymentTemplate.properties.template.parameters['eventGridRbacMode'] = Get-EventGridRbacModeParameter + # Snapshot and validate the existing topic before any nested deployment updates it. + $storageDeploymentTemplate.properties.parameters['existingSystemTopic'] = @{ + value = "[[if(empty(parameters('EGSystemTopicName')), createObject(), reference(variables('EGTopicResourceId'), '2025-02-15', 'Full'))]" + } + $storageDeploymentTemplate.properties.template.parameters['existingSystemTopic'] = @{ type = 'object' } + $storageDeploymentTemplate.properties.parameters['existingSystemTopicMatchesStorageAccount'] = @{ + value = "[[if(empty(parameters('EGSystemTopicName')), true(), equals(toLower(reference(variables('EGTopicResourceId'), '2025-02-15', 'Full').properties.source), toLower(variables('storageAccountId'))))]" + } + $storageDeploymentTemplate.properties.template.parameters['existingSystemTopicMatchesStorageAccount'] = @{ + type = 'bool' + allowedValues = @($true) + metadata = @{ + description = 'The existing Event Grid system topic must use the selected storage account as its source.' + } + } $storageDeploymentTemplate.properties.template['variables'] = [ordered]@{} $storageVariableNames = @( - 'connectorName', 'blobContainerUriPart', 'storageAccountName', 'blobContainerName', - 'queueName', 'dlqName', 'notificationQueueResourceId', 'dlqResourceId', - 'EGSystemTopicDefaultName', 'EGSystemTopicName', 'EgSubscriptionName', - 'storageBlobContributorRoleId', 'storageQueueContributorRoleId', - 'blobRaGuid', 'notificationQueueRaGuid', 'dlqRaGuid', - 'eventGridTopicDeploymentName', 'eventGridTopicDeploymentId', - 'eventGridQueueSenderDeploymentName', 'eventGridQueueSenderDeploymentId' + 'storageAccountName', 'storageAccountId', 'blobContainerName', + 'queueName', 'dlqName', 'EGSystemTopicName', 'EGTopicResourceId', 'EgSubscriptionName' ) + if (-not $isManagedServicePrincipal) { + $storageDeploymentTemplate.properties.parameters['principalId'] = @{ value = "[[parameters('principalId')]" } + $storageDeploymentTemplate.properties.template.parameters['principalId'] = @{ type = 'securestring' } + $storageVariableNames += @( + 'storageBlobContributorRoleId', 'storageQueueContributorRoleId', + 'blobRaGuid', 'notificationQueueRaGuid', 'dlqRaGuid', 'dlqResourceId' + ) + } foreach ($name in $storageVariableNames) { if (-not $resourceVariables.Contains($name)) { throw "Missing storage variable '$name' for managed-identity Event Grid delivery." } - $storageDeploymentTemplate.properties.template.variables[$name] = $resourceVariables[$name] + $storageDeploymentTemplate.properties.parameters[$name] = @{ value = "[[variables('$name')]" } + $storageDeploymentTemplate.properties.template.parameters[$name] = @{ type = 'string' } + $storageDeploymentTemplate.properties.template.variables[$name] = "[[parameters('$name')]" } + $storageDeploymentTemplate.properties.template.variables['notificationQueueResourceId'] = "[[resourceId('Microsoft.Storage/storageAccounts/queueServices/queues', variables('storageAccountName'), 'default', variables('queueName'))]" + $storageDeploymentTemplate.properties.template.variables['eventGridTopicDeploymentName'] = "[[concat('PrepareEventGridTopic-', uniqueString(variables('EGTopicResourceId')))]" + $storageDeploymentTemplate.properties.template.variables['eventGridTopicDeploymentId'] = "[[resourceId('Microsoft.Resources/deployments', variables('eventGridTopicDeploymentName'))]" + $storageDeploymentTemplate.properties.template.variables['eventGridQueueSenderDeploymentName'] = "[[concat('EventGridQueueSender-', uniqueString(variables('EGTopicResourceId'), variables('queueName')))]" + $storageDeploymentTemplate.properties.template.variables['eventGridQueueSenderDeploymentId'] = "[[resourceId('Microsoft.Resources/deployments', variables('eventGridQueueSenderDeploymentName'))]" $eventSubscriptionResource = $storageDeploymentTemplate.properties.template.resources | Where-Object { $_.type -eq 'Microsoft.EventGrid/systemTopics/eventSubscriptions' } | Select-Object -First 1 $eventSubscriptionResource.apiVersion = '2025-02-15' $destination = $eventSubscriptionResource.properties.destination - $destination.properties.resourceId = "[[resourceId(parameters('StorageAccountSubscription'), parameters('StorageAccountResourceGroupName'), 'Microsoft.Storage/storageAccounts', variables('storageAccountName'))]" + $destination.properties.resourceId = "[[variables('storageAccountId')]" + $eventSubscriptionResource.properties.filter.subjectBeginsWith = "[[format('{0}/{1}/blobs/{2}', '/blobServices/default/containers', variables('blobContainerName'), if(empty(parameters('blobFolderName')), '', concat(parameters('blobFolderName'),'/')))]" $eventSubscriptionResource.properties = [ordered]@{ deliveryWithResourceIdentity = [ordered]@{ identity = @{ type = 'SystemAssigned' } @@ -321,11 +362,12 @@ function Set-ResourceVariables { $variables["nestedDeploymentId"] = "[[resourceId(parameters('StorageAccountResourceGroupName'), 'Microsoft.Resources/deployments', variables('nestedDeploymentName'))]" if ($eventGridUseManagedIdentity) { + $variables['storageAccountId'] = "[[resourceId(parameters('StorageAccountSubscription'), parameters('StorageAccountResourceGroupName'), 'Microsoft.Storage/storageAccounts', variables('storageAccountName'))]" + $variables['notificationQueueResourceId'] = "[[resourceId(parameters('StorageAccountSubscription'), parameters('StorageAccountResourceGroupName'), 'Microsoft.Storage/storageAccounts/queueServices/queues', variables('storageAccountName'), 'default', variables('queueName'))]" + $variables['dlqResourceId'] = "[[resourceId(parameters('StorageAccountSubscription'), parameters('StorageAccountResourceGroupName'), 'Microsoft.Storage/storageAccounts/queueServices/queues', variables('storageAccountName'), 'default', variables('dlqName'))]" + $variables['EGTopicResourceId'] = "[[resourceId(parameters('StorageAccountSubscription'), parameters('StorageAccountResourceGroupName'), 'Microsoft.EventGrid/systemTopics', variables('EGSystemTopicName'))]" + $variables['EgSubscriptionResourceId'] = "[[resourceId(parameters('StorageAccountSubscription'), parameters('StorageAccountResourceGroupName'), 'Microsoft.EventGrid/systemTopics/eventSubscriptions', variables('EGSystemTopicName'), variables('EgSubscriptionName'))]" $variables['nestedDeploymentId'] = "[[resourceId(parameters('StorageAccountSubscription'), parameters('StorageAccountResourceGroupName'), 'Microsoft.Resources/deployments', variables('nestedDeploymentName'))]" - $variables['eventGridTopicDeploymentName'] = "[[concat('PrepareEventGridTopic-', uniqueString(parameters('StorageAccountSubscription'), parameters('StorageAccountResourceGroupName'), variables('EGSystemTopicName')))]" - $variables['eventGridTopicDeploymentId'] = "[[resourceId(parameters('StorageAccountSubscription'), parameters('StorageAccountResourceGroupName'), 'Microsoft.Resources/deployments', variables('eventGridTopicDeploymentName'))]" - $variables['eventGridQueueSenderDeploymentName'] = "[[concat('EventGridQueueSender-', uniqueString(parameters('StorageAccountSubscription'), parameters('StorageAccountResourceGroupName'), variables('EGSystemTopicName'), variables('queueName')))]" - $variables['eventGridQueueSenderDeploymentId'] = "[[resourceId(parameters('StorageAccountSubscription'), parameters('StorageAccountResourceGroupName'), 'Microsoft.Resources/deployments', variables('eventGridQueueSenderDeploymentName'))]" } $templateContentConnections.properties.mainTemplate.variables = $variables diff --git a/Tools/Create-Azure-Sentinel-Solution/tests/EventGridManagedIdentity.Tests.ps1 b/Tools/Create-Azure-Sentinel-Solution/tests/EventGridManagedIdentity.Tests.ps1 index 3b010f14121..54b68b9c92e 100644 --- a/Tools/Create-Azure-Sentinel-Solution/tests/EventGridManagedIdentity.Tests.ps1 +++ b/Tools/Create-Azure-Sentinel-Solution/tests/EventGridManagedIdentity.Tests.ps1 @@ -8,7 +8,14 @@ pwsh -NoProfile -File Tools\Create-Azure-Sentinel-Solution\tests\EventGridManage These dependency-free tests assert generated ARM expressions and resource wiring. They do not evaluate ARM expressions or verify Azure identity/RBAC runtime behavior. +Optional -ConnectorDirectory and -ReferenceTemplatePath inputs accept the generated +GitHubAuditBlobConnector source files and expected mainTemplate.json for comparison. #> +param( + [string]$ReferenceTemplatePath, + [string]$ConnectorDirectory +) + $ErrorActionPreference = 'Stop' . (Join-Path $PSScriptRoot '..\common\storageAccountDeploymentTemplate.ps1') . (Join-Path $PSScriptRoot '..\common\eventGridManagedIdentityDeploymentTemplate.ps1') @@ -30,7 +37,28 @@ function Assert-Throws([scriptblock]$Action, [string]$Message) { throw "Expected exception: $Message" } -function Get-TestFixture([bool]$ManagedIdentity, $AdvancedFilters = $null) { +function Assert-Equivalent($Actual, $Expected, [string]$Path = '$') { + try { + if ($Expected -is [System.Collections.IDictionary]) { + Assert-Equal @($Actual.Keys | Sort-Object) @($Expected.Keys | Sort-Object) + foreach ($key in $Expected.Keys) { + Assert-Equivalent $Actual[$key] $Expected[$key] "$Path.$key" + } + } + elseif ($Expected -is [array]) { + Assert-Equal $Actual.Count $Expected.Count + for ($i = 0; $i -lt $Expected.Count; $i++) { + Assert-Equivalent $Actual[$i] $Expected[$i] "$Path[$i]" + } + } + else { + Assert-Equal $Actual $Expected + } + } + catch { throw "${Path}: $($_.Exception.Message)" } +} + +function Get-TestFixture([bool]$ManagedIdentity, $AdvancedFilters = $null, [bool]$ManagedCollector = $false) { # Set-ResourceVariables uses its caller's template and variables, as in production. $variables = [ordered]@{} $templateContentConnections = [pscustomobject]@{ @@ -42,7 +70,8 @@ function Get-TestFixture([bool]$ManagedIdentity, $AdvancedFilters = $null) { $deployment = Get-StorageAccountDeploymentTemplate ` -eventGridUseManagedIdentity $ManagedIdentity ` -resourceVariables $templateContentConnections.properties.mainTemplate.variables ` - -eventGridAdvancedFilters $AdvancedFilters + -eventGridAdvancedFilters $AdvancedFilters ` + -isManagedServicePrincipal $ManagedCollector # Check the serialized contract, not PowerShell dictionary implementation details. return ($deployment | ConvertTo-Json -Depth 100 | ConvertFrom-Json -AsHashtable) } @@ -74,36 +103,47 @@ $tests = [ordered]@{ Assert-Equal $child.properties.expressionEvaluationOptions.scope 'inner' Assert-Equal $child.properties.mode 'Incremental' } - foreach ($name in $inner.parameters.Keys) { + foreach ($name in @('StorageAccountLocation', 'blobFolderName', 'eventGridRbacMode')) { Assert-Equal $managed.properties.parameters[$name].value "[[parameters('$name')]" } + foreach ($name in @('storageAccountName', 'storageAccountId', 'blobContainerName', 'queueName', 'dlqName', 'EGSystemTopicName', 'EGTopicResourceId', 'EgSubscriptionName')) { + Assert-Equal $managed.properties.parameters[$name].value "[[variables('$name')]" + Assert-Equal $inner.variables[$name] "[[parameters('$name')]" + } Assert-Equal $managed.subscriptionId "[[parameters('StorageAccountSubscription')]" Assert-Equal $managed.resourceGroup "[[parameters('StorageAccountResourceGroupName')]" } 'Existing topic is read before the inner update; new topic skips reference' = { - Assert-Equal $topicDeployment.properties.parameters.existingTopic.value "[[if(empty(parameters('EGSystemTopicName')), createObject(), reference(resourceId(parameters('StorageAccountSubscription'), parameters('StorageAccountResourceGroupName'), 'Microsoft.EventGrid/systemTopics', variables('EGSystemTopicName')), '2025-02-15', 'Full'))]" + Assert-Equal $managed.properties.parameters.existingSystemTopic.value "[[if(empty(parameters('EGSystemTopicName')), createObject(), reference(variables('EGTopicResourceId'), '2025-02-15', 'Full'))]" + Assert-Equal $topicDeployment.properties.parameters.existingSystemTopic.value "[[parameters('existingSystemTopic')]" Assert-Equal $topic.ContainsKey('condition') $false - Assert-Equal $inner.variables.EGSystemTopicName "[[if(empty(parameters('EGSystemTopicName')), variables('EGSystemTopicDefaultName'), parameters('EGSystemTopicName'))]" Assert-Equal $topic.name "[[parameters('topicName')]" } + 'Existing topic source must match before its identity is updated' = { + Assert-Equal $managed.properties.parameters.existingSystemTopicMatchesStorageAccount.value "[[if(empty(parameters('EGSystemTopicName')), true(), equals(toLower(reference(variables('EGTopicResourceId'), '2025-02-15', 'Full').properties.source), toLower(variables('storageAccountId'))))]" + Assert-Equal $inner.parameters.existingSystemTopicMatchesStorageAccount.type 'bool' + Assert-Equal $inner.parameters.existingSystemTopicMatchesStorageAccount.allowedValues @($true) + Assert-Equal $topicDeployment.condition "[[parameters('existingSystemTopicMatchesStorageAccount')]" + } 'Identity expressions handle missing/null identities and retain user-assigned identities' = { - Assert-Equal $topicTemplate.variables.existingIdentity "[[if(contains(parameters('existingTopic'), 'identity'), coalesce(parameters('existingTopic').identity, createObject()), createObject())]" - Assert-Equal $topicTemplate.variables.userAssignedIdentities "[[if(contains(variables('existingIdentity'), 'userAssignedIdentities'), coalesce(variables('existingIdentity').userAssignedIdentities, createObject()), createObject())]" - Assert-Equal $topicTemplate.variables.topicIdentity "[[if(empty(variables('userAssignedIdentities')), createObject('type', 'SystemAssigned'), createObject('type', 'SystemAssigned, UserAssigned', 'userAssignedIdentities', variables('userAssignedIdentities')))]" + Assert-Equal $topicTemplate.variables.existingTopicIdentity "[[if(contains(parameters('existingSystemTopic'), 'identity'), coalesce(parameters('existingSystemTopic').identity, createObject()), createObject())]" + Assert-Equal $topicTemplate.variables.existingUserAssignedIdentities "[[if(contains(variables('existingTopicIdentity'), 'userAssignedIdentities'), coalesce(variables('existingTopicIdentity').userAssignedIdentities, createObject()), createObject())]" + Assert-Equal $topicTemplate.variables.topicIdentity "[[if(empty(variables('existingUserAssignedIdentities')), createObject('type', 'SystemAssigned'), createObject('type', 'SystemAssigned, UserAssigned', 'userAssignedIdentities', variables('existingUserAssignedIdentities')))]" Assert-Equal $topic.identity "[[variables('topicIdentity')]" } 'Topic update retains supplied tags and storage source' = { - Assert-Equal $topic.tags "[[if(contains(parameters('existingTopic'), 'tags'), coalesce(parameters('existingTopic').tags, createObject()), createObject())]" - Assert-Equal $topic.properties.source "[[parameters('sourceId')]" - Assert-Equal $topic.properties.topicType 'microsoft.storage.storageaccounts' + Assert-Equal $topic.tags "[[if(contains(parameters('existingSystemTopic'), 'tags'), coalesce(parameters('existingSystemTopic').tags, createObject()), createObject())]" + Assert-Equal $topic.location "[[if(empty(parameters('existingSystemTopic')), parameters('StorageAccountLocation'), parameters('existingSystemTopic').location)]" + Assert-Equal $topic.properties.source "[[if(empty(parameters('existingSystemTopic')), parameters('sourceId'), parameters('existingSystemTopic').properties.source)]" + Assert-Equal $topic.properties.topicType "[[if(empty(parameters('existingSystemTopic')), 'microsoft.storage.storageaccounts', parameters('existingSystemTopic').properties.topicType)]" Assert-Equal $topicDeployment.properties.parameters.sourceId.value $storageId } 'Sender deployment consumes the completed topic deployment principal output' = { Assert-Equal $topicTemplate.outputs.principalId.value "[[reference(resourceId('Microsoft.EventGrid/systemTopics', parameters('topicName')), '2025-02-15', 'Full').identity.principalId]" - Assert-Equal $senderDeployment.properties.parameters.principalId.value "[[reference(variables('eventGridTopicDeploymentId'), '2025-04-01').outputs.principalId.value]" + Assert-Equal $senderDeployment.properties.parameters.principalId.value "[[if(equals(parameters('eventGridRbacMode'), 'CreateRoleAssignment'), reference(variables('eventGridTopicDeploymentId'), '2025-04-01').outputs.principalId.value, '')]" Assert-Equal $senderDeployment.dependsOn @( "[[variables('eventGridTopicDeploymentId')]", - "[[resourceId(parameters('StorageAccountSubscription'), parameters('StorageAccountResourceGroupName'), 'Microsoft.Storage/storageAccounts/queueServices/queues', variables('storageAccountName'), 'default', variables('queueName'))]" + "[[variables('notificationQueueResourceId')]" ) Assert-Equal $sender.properties.principalId "[[parameters('principalId')]" } @@ -129,11 +169,30 @@ $tests = [ordered]@{ "[[variables('notificationQueueResourceId')]" ) foreach ($prefix in @('eventGridTopic', 'eventGridQueueSender')) { - Assert-Equal $inner.variables["${prefix}DeploymentId"] "[[resourceId(parameters('StorageAccountSubscription'), parameters('StorageAccountResourceGroupName'), 'Microsoft.Resources/deployments', variables('${prefix}DeploymentName'))]" + Assert-Equal $inner.variables["${prefix}DeploymentId"] "[[resourceId('Microsoft.Resources/deployments', variables('${prefix}DeploymentName'))]" } Assert-Equal $topicDeployment.name "[[variables('eventGridTopicDeploymentName')]" Assert-Equal $senderDeployment.name "[[variables('eventGridQueueSenderDeploymentName')]" } + 'Folder filtering is optional and uses a complete container/blob prefix' = { + Assert-Equal $managed.properties.parameters.blobFolderName.value "[[parameters('blobFolderName')]" + Assert-Equal $inner.parameters.blobFolderName.defaultValue '' + Assert-Equal $subscription.properties.filter.subjectBeginsWith "[[format('{0}/{1}/blobs/{2}', '/blobServices/default/containers', variables('blobContainerName'), if(empty(parameters('blobFolderName')), '', concat(parameters('blobFolderName'),'/')))]" + } + 'Managed collectors do not create Sentinel collector grants; service-principal collectors retain them' = { + foreach ($enabled in @($false, $true)) { + $managedCollector = Get-TestFixture $enabled $null $true + Assert-Equal @($managedCollector.properties.template.resources | Where-Object type -like '*/providers/roleAssignments').Count 0 + $legacyCollector = Get-TestFixture $enabled + Assert-Equal @($legacyCollector.properties.template.resources | Where-Object type -like '*/providers/roleAssignments').Count 3 + if ($enabled) { + Assert-Equal $legacyCollector.properties.parameters.principalId.value "[[parameters('principalId')]" + Assert-Equal $legacyCollector.properties.template.variables.blobRaGuid "[[parameters('blobRaGuid')]" + } + } + Assert-Equal $inner.resources.Count 5 + Assert-Equal $inner.parameters.ContainsKey('principalId') $false + } 'Subscription uses managed-identity delivery, not the legacy destination property' = { Assert-Equal $subscription.properties.ContainsKey('destination') $false Assert-Equal $subscription.properties.deliveryWithResourceIdentity.identity.type 'SystemAssigned' @@ -150,7 +209,9 @@ $tests = [ordered]@{ $event = Get-OnlyResource $filtered.properties.template 'Microsoft.EventGrid/systemTopics/eventSubscriptions' Assert-Equal $event.properties.filter.advancedFilters $filters Assert-Equal $event.properties.filter.includedEventTypes @('Microsoft.Storage.BlobCreated') - Assert-Equal $event.properties.filter.subjectBeginsWith "[[format('{0}/{1}', '/blobServices/default/containers', variables('blobContainerName'))]" + if (-not $enabled) { + Assert-Equal $event.properties.filter.subjectBeginsWith "[[format('{0}/{1}', '/blobServices/default/containers', variables('blobContainerName'))]" + } foreach ($emptyFilters in @($null, @())) { $unfiltered = Get-TestFixture $enabled $emptyFilters $event = Get-OnlyResource $unfiltered.properties.template 'Microsoft.EventGrid/systemTopics/eventSubscriptions' @@ -180,13 +241,108 @@ $tests = [ordered]@{ 'Storage variables are required for managed-identity Event Grid delivery.' $incomplete = [ordered]@{} foreach ($key in $inner.variables.Keys) { $incomplete[$key] = $inner.variables[$key] } - $incomplete.Remove('eventGridTopicDeploymentId') + $incomplete.Remove('EGTopicResourceId') Assert-Throws { Get-StorageAccountDeploymentTemplate -eventGridUseManagedIdentity $true -resourceVariables $incomplete } ` - "Missing storage variable 'eventGridTopicDeploymentId' for managed-identity Event Grid delivery." + "Missing storage variable 'EGTopicResourceId' for managed-identity Event Grid delivery." + } + 'Managed-identity outer resource IDs explicitly select the storage subscription and resource group' = { + $variables = [ordered]@{} + $templateContentConnections = [pscustomobject]@{ + properties = [pscustomobject]@{ mainTemplate = [pscustomobject]@{ variables = $variables } } + } + Set-ResourceVariables -eventGridUseManagedIdentity $true + foreach ($name in @('storageAccountId', 'notificationQueueResourceId', 'dlqResourceId', 'EGTopicResourceId', 'EgSubscriptionResourceId', 'nestedDeploymentId')) { + Assert-Equal $variables[$name].StartsWith("[[resourceId(parameters('StorageAccountSubscription'), parameters('StorageAccountResourceGroupName'),") $true + } + } +} + +if ($ReferenceTemplatePath) { + $tests['Generated storage deployment matches the supplied GitHub reference'] = { + $reference = Get-Content -LiteralPath $ReferenceTemplatePath -Raw | ConvertFrom-Json -AsHashtable -Depth 100 + $connections = @($reference.resources | Where-Object { $_.properties.contentKind -eq 'ResourcesDataConnector' }) + Assert-Equal $connections.Count 1 + $referenceDeployment = Get-OnlyResource $connections[0].properties.mainTemplate 'Microsoft.Resources/deployments' + $referenceSubscription = Get-OnlyResource $referenceDeployment.properties.template 'Microsoft.EventGrid/systemTopics/eventSubscriptions' + $actual = Get-TestFixture $true $referenceSubscription.properties.filter.advancedFilters $true + Assert-Equivalent $actual $referenceDeployment + } +} + +$tests['CCF packaging preserves source content, optional parameters and both collector authentication modes'] = { + $repoRoot = Split-Path (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent) -Parent + $sourceDirectory = if ($ConnectorDirectory) { $ConnectorDirectory } else { + Join-Path $repoRoot 'Solutions\GitHub\Data Connectors\GitHubAuditLogs_AzStorage' + } + $fixtureRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("sentinel-blob-packaging-test-" + [guid]::NewGuid().ToString('N')) + $fixtureSolution = Join-Path $fixtureRoot 'Solutions\GitHub' + $fixtureConnector = Join-Path $fixtureSolution 'Data Connectors\GitHubAuditLogs_AzStorage' + try { + $null = New-Item -ItemType Directory -Path $fixtureConnector + foreach ($name in @('ConnectorDefinition.json', 'PollingConfig.json', 'DCR.json')) { + Copy-Item -LiteralPath (Join-Path $sourceDirectory $name) -Destination $fixtureConnector + } + Copy-Item -LiteralPath (Join-Path $repoRoot 'Solutions\GitHub\Data Connectors\GitHubAuditLogs_AzStorage\table_GitHubAuditLogsV3.json') -Destination $fixtureConnector + $sourceDefinition = Get-Content -LiteralPath (Join-Path $fixtureConnector 'ConnectorDefinition.json') -Raw | ConvertFrom-Json -AsHashtable -Depth 100 + $sourceDcr = Get-Content -LiteralPath (Join-Path $fixtureConnector 'DCR.json') -Raw | ConvertFrom-Json -AsHashtable -Depth 100 + $pollingPath = Join-Path $fixtureConnector 'PollingConfig.json' + $polling = Get-Content -LiteralPath $pollingPath -Raw | ConvertFrom-Json -Depth 100 + foreach ($authType in @('ManagedServicePrincipal', 'ServicePrincipal')) { + $polling.properties.auth.type = $authType + $polling | ConvertTo-Json -Depth 100 | Set-Content -LiteralPath $pollingPath + . (Join-Path $PSScriptRoot '..\common\commonFunctions.ps1') + . (Join-Path $PSScriptRoot '..\common\get-ccp-details.ps1') + . (Join-Path $PSScriptRoot '..\common\createCCPConnector.ps1') + $data = Get-Content -LiteralPath (Join-Path $repoRoot 'Solutions\GitHub\data\Solution_GitHub.json') -Raw | ConvertFrom-Json + $data.'Data Connectors' = @('Data Connectors/GitHubAuditLogs_AzStorage/ConnectorDefinition.json') + $metadata = Get-Content -LiteralPath (Join-Path $repoRoot 'Solutions\GitHub\SolutionMetadata.json') -Raw | ConvertFrom-Json + $solutionBase = (Join-Path $fixtureRoot 'Solutions') + [System.IO.Path]::DirectorySeparatorChar + $dict = @(Get-CCP-Dict -dataFileMetadata $data -baseFolderPath $solutionBase -solutionName 'GitHub' -DCFolderName 'Data Connectors') + Assert-Equal $dict.Count 1 + $tables = @(GetCCPTableFilePaths -existingCCPDict $dict -baseFolderPath $solutionBase -solutionName 'GitHub' -DCFolderName 'Data Connectors') + createCCPConnectorResources -contentResourceDetails (returnContentResources $data.Version) ` + -dataFileMetadata $data -solutionFileMetadata $metadata -dcFolderName 'Data Connectors' ` + -ccpDict $dict -solutionBasePath $solutionBase -solutionName 'GitHub' -ccpTables $tables -ccpTablesCounter 1 + # Use the V3 serializer's depth to catch nested-template truncation as well as parameter merging. + $packaged = $global:baseMainTemplate | ConvertTo-Json -Depth 50 -WarningAction Stop | ConvertFrom-Json -AsHashtable -Depth 100 + $connections = @($packaged.resources | Where-Object { $_.properties.contentKind -eq 'ResourcesDataConnector' }) + Assert-Equal $connections.Count 1 + $connectionTemplate = $connections[0].properties.mainTemplate + $connector = Get-OnlyResource $connectionTemplate 'Microsoft.OperationalInsights/workspaces/providers/dataConnectors' + Assert-Equal $connector.properties.auth.type $authType + if ($authType -eq 'ManagedServicePrincipal') { + Assert-Equal $connector.properties.connectorDefinitionName "[[parameters('connectorDefinitionName')]" + } + Assert-Equal $connector.ContainsKey('eventGridUseManagedIdentity') $false + Assert-Equal $connector.ContainsKey('eventGridAdvancedFilters') $false + Assert-Equal $connector.properties.response.isGzipCompressed $true + Assert-Equal $connectionTemplate.parameters.blobFolderName.defaultValue '' + Assert-Equal $connectionTemplate.parameters.blobFolderName.ContainsKey('minLength') $false + Assert-Equal $connectionTemplate.parameters.eventGridRbacMode.allowedValues @('CreateRoleAssignment', 'UseExistingRoleAssignment') + Assert-Equal $connectionTemplate.parameters.eventGridRbacMode.defaultValue 'CreateRoleAssignment' + Assert-Equal $connectionTemplate.parameters.EGSystemTopicName.defaultValue '' + Assert-Equal $connectionTemplate.parameters.EGSystemTopicName.ContainsKey('minLength') $false + $definition = Get-OnlyResource $packaged 'Microsoft.OperationalInsights/workspaces/providers/dataConnectorDefinitions' + Assert-Equivalent $definition.properties.connectorUiConfig $sourceDefinition.properties.connectorUiConfig + $definitionTemplate = @($packaged.resources | Where-Object { $_.properties.contentKind -eq 'DataConnector' })[0].properties.mainTemplate + $dcr = Get-OnlyResource $definitionTemplate 'Microsoft.Insights/dataCollectionRules' + Assert-Equivalent $dcr.properties.streamDeclarations $sourceDcr.properties.streamDeclarations + Assert-Equivalent $dcr.properties.dataFlows $sourceDcr.properties.dataFlows + $deployment = Get-OnlyResource $connectionTemplate 'Microsoft.Resources/deployments' + Assert-Equal @($deployment.properties.template.resources | Where-Object type -like '*/providers/roleAssignments').Count $(if ($authType -eq 'ManagedServicePrincipal') { 0 } else { 3 }) + if ($ReferenceTemplatePath -and $authType -eq 'ManagedServicePrincipal') { + $reference = Get-Content -LiteralPath $ReferenceTemplatePath -Raw | ConvertFrom-Json -AsHashtable -Depth 100 + $referenceConnections = @($reference.resources | Where-Object { $_.properties.contentKind -eq 'ResourcesDataConnector' })[0].properties.mainTemplate + Assert-Equivalent $deployment (Get-OnlyResource $referenceConnections 'Microsoft.Resources/deployments') + } + } + } + finally { + if (Test-Path -LiteralPath $fixtureRoot) { Remove-Item -LiteralPath $fixtureRoot -Recurse -Force } } } -$managed = Get-TestFixture $true +$managed = Get-TestFixture $true $null $true $inner = $managed.properties.template $children = @($inner.resources | Where-Object type -eq 'Microsoft.Resources/deployments') $topicDeployment = @($children | Where-Object name -eq "[[variables('eventGridTopicDeploymentName')]") @@ -200,7 +356,7 @@ $senderTemplate = $senderDeployment.properties.template $topic = Get-OnlyResource $topicTemplate 'Microsoft.EventGrid/systemTopics' $sender = Get-OnlyResource $senderTemplate 'Microsoft.Authorization/roleAssignments' $subscription = Get-OnlyResource $inner 'Microsoft.EventGrid/systemTopics/eventSubscriptions' -$storageId = "[[resourceId(parameters('StorageAccountSubscription'), parameters('StorageAccountResourceGroupName'), 'Microsoft.Storage/storageAccounts', variables('storageAccountName'))]" +$storageId = "[[variables('storageAccountId')]" $filters = @( [ordered]@{ operatorType = 'StringEndsWith'; key = 'subject'; values = @('.json.gz') }, [ordered]@{ operatorType = 'StringIn'; key = 'data.api'; values = @('PutBlob', 'FlushWithClose') } From 769f21247e5036f9bdd05fcccdb62c7d077adbb1 Mon Sep 17 00:00:00 2001 From: Alex Li Date: Mon, 26 Oct 2026 16:36:21 -0500 Subject: [PATCH 6/6] Fix Storage Blob connector package dependency Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Templates/Connector_StorageBlob_CCF_templateV2.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DataConnectors/Templates/Connector_StorageBlob_CCF_templateV2.json b/DataConnectors/Templates/Connector_StorageBlob_CCF_templateV2.json index bfaab85896b..cd91a75c68f 100644 --- a/DataConnectors/Templates/Connector_StorageBlob_CCF_templateV2.json +++ b/DataConnectors/Templates/Connector_StorageBlob_CCF_templateV2.json @@ -1006,8 +1006,8 @@ "criteria": [ { "kind": "DataConnector", - "contentId": "[variables('dataConnectorVersionConnectorDefinition')]", - "version": "[variables('_dataConnectorContentIdConnectorDefinition')]" + "contentId": "[variables('_dataConnectorContentIdConnectorDefinition')]", + "version": "[variables('dataConnectorVersionConnectorDefinition')]" } ] },