diff --git a/Tools/CCF-Pull-Connector-Nested-Accelerator/ContosoIncidents/Data Connectors/ContosoIncidents_CCF/ContosoIncidents_ConnectorDefinition.json b/Tools/CCF-Pull-Connector-Nested-Accelerator/ContosoIncidents/Data Connectors/ContosoIncidents_CCF/ContosoIncidents_ConnectorDefinition.json new file mode 100644 index 00000000000..71b02bcf7f1 --- /dev/null +++ b/Tools/CCF-Pull-Connector-Nested-Accelerator/ContosoIncidents/Data Connectors/ContosoIncidents_CCF/ContosoIncidents_ConnectorDefinition.json @@ -0,0 +1,112 @@ +{ + "name": "ContosoIncidentsConnector", + "apiVersion": "2022-09-01-preview", + "type": "Microsoft.SecurityInsights/dataConnectorDefinitions", + "location": "{{location}}", + "kind": "Customizable", + "availability": { + "isPreview": true + }, + "properties": { + "connectorUiConfig": { + "id": "ContosoIncidentsConnector", + "title": "Contoso Incidents (CCF Nested API Accelerator)", + "publisher": "Contoso", + "descriptionMarkdown": "The **Contoso Incidents** connector demonstrates the CCF nested API polling pattern. It chains two REST API calls: a list endpoint that returns incident identifiers, followed by a detail endpoint called once per identifier. Deploy the included mock Contoso Incident API (Azure Function App) to observe the nested polling pattern end-to-end without a real data source.", + "graphQueriesTableName": "ContosoIncidents_CL", + "graphQueries": [ + { + "metricName": "Incidents ingested", + "legend": "ContosoIncidents_CL", + "baseQuery": "{{graphQueriesTableName}}" + } + ], + "sampleQueries": [ + { + "description": "All ingested incidents", + "query": "ContosoIncidents_CL\n| sort by TimeGenerated desc" + }, + { + "description": "High and Critical severity incidents", + "query": "ContosoIncidents_CL\n| where Severity in ('Critical', 'High')\n| sort by TimeGenerated desc" + }, + { + "description": "Active incidents", + "query": "ContosoIncidents_CL\n| where Status == 'Active'\n| project TimeGenerated, IncidentId, Title, Severity, AffectedUser, SourceIp" + } + ], + "dataTypes": [ + { + "name": "ContosoIncidents_CL", + "lastDataReceivedQuery": "ContosoIncidents_CL\n| summarize Time = max(TimeGenerated)\n| where isnotempty(Time)" + } + ], + "connectivityCriteria": [ + { + "type": "IsConnectedQuery", + "value": [ + "ContosoIncidents_CL\n| summarize LastLogReceived = max(TimeGenerated)\n| project IsConnected = LastLogReceived > ago(1d)" + ] + } + ], + "permissions": { + "resourceProvider": [ + { + "provider": "Microsoft.OperationalInsights/workspaces", + "permissionsDisplayText": "Read and write permissions on the Log Analytics workspace are required.", + "providerDisplayName": "Workspace", + "scope": "Workspace", + "requiredPermissions": { + "write": true, + "read": true, + "delete": true + } + } + ], + "customs": [ + { + "name": "Contoso Mock API", + "description": "The Contoso Mock API Function App must be deployed before configuring this connector. After deployment, retrieve the Function App API key to use in Step 2." + } + ] + }, + "instructionSteps": [ + { + "title": "STEP 1 — Deploy the Contoso Mock API", + "description": "Deploy the Azure Function App that serves as the mock Contoso Incident API:\n\n1. Deploy `MockApi/azuredeploy_MockApi.json` to a new resource group.\n2. Zip the `MockApi/` folder and run:\n```\naz functionapp deployment source config-zip --name --resource-group --src contosoapi.zip\n```\n3. Retrieve the Function App API key:\n```\naz functionapp keys list --name --resource-group --query functionKeys.default -o tsv\n```\n4. Note the `mockApiBaseUrl` deployment output (e.g. `https://contosomockapiXXXX.azurewebsites.net/api`).\n\nSee the [accelerator README](https://github.com/Azure/Azure-Sentinel/tree/master/Tools/CCF-Pull-Connector-Nested-Accelerator) for detailed steps or use GitHub Copilot agent mode with `agent-instructions.md` for fully automated deployment." + }, + { + "title": "STEP 2 — Connect to the Contoso Mock API", + "description": "Enter the Mock API base URL and Function App API key, then click **Connect**.", + "instructions": [ + { + "type": "Textbox", + "parameters": { + "label": "Mock API Base URL", + "placeholder": "https://contosomockapiXXXX.azurewebsites.net/api", + "type": "text", + "name": "mockApiBaseUrl" + } + }, + { + "type": "Textbox", + "parameters": { + "label": "Function App API Key", + "placeholder": "Enter your Function App API key", + "type": "password", + "name": "ApiKey" + } + }, + { + "type": "ConnectionToggleButton", + "parameters": { + "connectLabel": "connect", + "name": "connect" + } + } + ] + } + ] + } + } +} diff --git a/Tools/CCF-Pull-Connector-Nested-Accelerator/ContosoIncidents/Data Connectors/ContosoIncidents_CCF/ContosoIncidents_DCR.json b/Tools/CCF-Pull-Connector-Nested-Accelerator/ContosoIncidents/Data Connectors/ContosoIncidents_CCF/ContosoIncidents_DCR.json new file mode 100644 index 00000000000..52850e00108 --- /dev/null +++ b/Tools/CCF-Pull-Connector-Nested-Accelerator/ContosoIncidents/Data Connectors/ContosoIncidents_CCF/ContosoIncidents_DCR.json @@ -0,0 +1,38 @@ +{ + "type": "Microsoft.Insights/dataCollectionRules", + "apiVersion": "2022-06-01", + "name": "ContosoIncidents-DCR", + "location": "{{location}}", + "properties": { + "dataCollectionEndpointId": "{{dataCollectionEndpoint}}", + "streamDeclarations": { + "Custom-ContosoIncidents_CL": { + "columns": [ + { "name": "TimeGenerated", "type": "datetime" }, + { "name": "incidentId", "type": "string" }, + { "name": "title", "type": "string" }, + { "name": "severity", "type": "string" }, + { "name": "status", "type": "string" }, + { "name": "affectedUser", "type": "string" }, + { "name": "sourceIp", "type": "string" } + ] + } + }, + "destinations": { + "logAnalytics": [ + { + "workspaceResourceId": "{{workspaceResourceId}}", + "name": "sentinel-workspace" + } + ] + }, + "dataFlows": [ + { + "streams": [ "Custom-ContosoIncidents_CL" ], + "destinations": [ "sentinel-workspace" ], + "transformKql": "source | extend TimeGenerated = now() | project TimeGenerated, IncidentId = ['incidentId'], Title = ['title'], Severity = ['severity'], Status = ['status'], AffectedUser = ['affectedUser'], SourceIp = ['sourceIp']", + "outputStream": "Custom-ContosoIncidents_CL" + } + ] + } +} diff --git a/Tools/CCF-Pull-Connector-Nested-Accelerator/ContosoIncidents/Data Connectors/ContosoIncidents_CCF/ContosoIncidents_PollerConfig.json b/Tools/CCF-Pull-Connector-Nested-Accelerator/ContosoIncidents/Data Connectors/ContosoIncidents_CCF/ContosoIncidents_PollerConfig.json new file mode 100644 index 00000000000..5206f6c0a28 --- /dev/null +++ b/Tools/CCF-Pull-Connector-Nested-Accelerator/ContosoIncidents/Data Connectors/ContosoIncidents_CCF/ContosoIncidents_PollerConfig.json @@ -0,0 +1,65 @@ +{ + "type": "Microsoft.SecurityInsights/dataConnectors", + "apiVersion": "2024-09-01", + "name": "ContosoIncidents", + "location": "{{location}}", + "kind": "RestApiPoller", + "properties": { + "connectorDefinitionName": "ContosoIncidentsConnector", + "dataType": "ContosoIncidents_CL", + "dcrConfig": { + "streamName": "Custom-ContosoIncidents_CL", + "dataCollectionEndpoint": "{{dataCollectionEndpoint}}", + "dataCollectionRuleImmutableId": "{{dataCollectionRuleImmutableId}}" + }, + "auth": { + "type": "APIKey", + "ApiKey": "{{ApiKey}}", + "ApiKeyName": "x-functions-key" + }, + "request": { + "apiEndpoint": "{{mockApiBaseUrl}}/incidents", + "httpMethod": "GET", + "queryWindowInMin": 5, + "queryTimeFormat": "yyyy-MM-ddTHH:mm:ssZ", + "startTimeAttributeName": "startTime", + "endTimeAttributeName": "endTime", + "headers": { + "Accept": "application/json" + } + }, + "response": { + "eventsJsonPaths": [ + "$.incidents" + ], + "format": "json" + }, + "stepInfo": { + "stepType": "Nested", + "nextSteps": [ + { + "stepId": "fetchIncidentDetails", + "stepPlaceholdersParsingKql": "source | project res = parse_json(data) | project incidentId = res.incidentId" + } + ] + }, + "stepCollectorConfigs": { + "fetchIncidentDetails": { + "shouldJoinNestedData": false, + "request": { + "httpMethod": "GET", + "apiEndpoint": "{{mockApiBaseUrl}}/incidents/$incidentId$/details", + "headers": { + "Accept": "application/json" + } + }, + "response": { + "eventsJsonPaths": [ + "$" + ], + "format": "json" + } + } + } + } +} diff --git a/Tools/CCF-Pull-Connector-Nested-Accelerator/ContosoIncidents/Data Connectors/ContosoIncidents_CCF/ContosoIncidents_Table.json b/Tools/CCF-Pull-Connector-Nested-Accelerator/ContosoIncidents/Data Connectors/ContosoIncidents_CCF/ContosoIncidents_Table.json new file mode 100644 index 00000000000..4912c7e2382 --- /dev/null +++ b/Tools/CCF-Pull-Connector-Nested-Accelerator/ContosoIncidents/Data Connectors/ContosoIncidents_CCF/ContosoIncidents_Table.json @@ -0,0 +1,50 @@ +[ + { + "name": "ContosoIncidents_CL", + "type": "Microsoft.OperationalInsights/workspaces/tables", + "apiVersion": "2022-10-01", + "properties": { + "plan": "Analytics", + "schema": { + "name": "ContosoIncidents_CL", + "columns": [ + { + "name": "TimeGenerated", + "type": "datetime", + "description": "Ingestion timestamp, set to the time the record was received by the data collection endpoint." + }, + { + "name": "IncidentId", + "type": "string", + "description": "Unique identifier of the incident (e.g. INC-001)." + }, + { + "name": "Title", + "type": "string", + "description": "Short description of the incident." + }, + { + "name": "Severity", + "type": "string", + "description": "Incident severity: Critical, High, Medium, or Low." + }, + { + "name": "Status", + "type": "string", + "description": "Current status of the incident: Active, Investigating, Resolved, or Closed." + }, + { + "name": "AffectedUser", + "type": "string", + "description": "UPN of the user associated with the incident." + }, + { + "name": "SourceIp", + "type": "string", + "description": "Source IP address associated with the incident." + } + ] + } + } + } +] diff --git a/Tools/CCF-Pull-Connector-Nested-Accelerator/ContosoIncidents/Data/Solution_ContosoIncidents.json b/Tools/CCF-Pull-Connector-Nested-Accelerator/ContosoIncidents/Data/Solution_ContosoIncidents.json new file mode 100644 index 00000000000..f7e788ca94d --- /dev/null +++ b/Tools/CCF-Pull-Connector-Nested-Accelerator/ContosoIncidents/Data/Solution_ContosoIncidents.json @@ -0,0 +1,21 @@ +{ + "Name": "ContosoIncidents", + "Author": "Microsoft", + "Logo": "", + "Description": "Demonstrates the CCF nested API polling pattern in Microsoft Sentinel. Includes a mock Contoso Incident API (Azure Function App) and a fully configured RestApiPoller connector that chains a list call to a detail call, ingesting complete incident records into the ContosoIncidents_CL table.", + "WorkbookDescription": [], + "Workbooks": [], + "Analytic Rules": [], + "Playbooks": [], + "Hunting Queries": [], + "Data Connectors": [ + "Data Connectors/ContosoIncidents_CCF/ContosoIncidents_ConnectorDefinition.json" + ], + "Parsers": [], + "Watchlists": [], + "BasePath": "C:\\GitHub\\Azure-Sentinel\\Tools\\CCF-Pull-Connector-Nested-Accelerator\\ContosoIncidents", + "Version": "1.0.0", + "Metadata": "SolutionMetadata.json", + "TemplateSpec": true, + "Is1PConnector": false +} diff --git a/Tools/CCF-Pull-Connector-Nested-Accelerator/ContosoIncidents/Package/3.0.0.zip b/Tools/CCF-Pull-Connector-Nested-Accelerator/ContosoIncidents/Package/3.0.0.zip new file mode 100644 index 00000000000..9118f4cff4a Binary files /dev/null and b/Tools/CCF-Pull-Connector-Nested-Accelerator/ContosoIncidents/Package/3.0.0.zip differ diff --git a/Tools/CCF-Pull-Connector-Nested-Accelerator/ContosoIncidents/Package/createUiDefinition.json b/Tools/CCF-Pull-Connector-Nested-Accelerator/ContosoIncidents/Package/createUiDefinition.json new file mode 100644 index 00000000000..6a87ecbe461 --- /dev/null +++ b/Tools/CCF-Pull-Connector-Nested-Accelerator/ContosoIncidents/Package/createUiDefinition.json @@ -0,0 +1,85 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/0.1.2-preview/CreateUIDefinition.MultiVm.json#", + "handler": "Microsoft.Azure.CreateUIDef", + "version": "0.1.2-preview", + "parameters": { + "config": { + "isWizard": false, + "basics": { + "description": "\n\n**Note:** Please refer to the following before installing the solution: \n\n• Review the solution [Release Notes](https://github.com/Azure/Azure-Sentinel/tree/master/Solutions/ContosoIncidents/ReleaseNotes.md)\n\n • There may be [known issues](https://aka.ms/sentinelsolutionsknownissues) pertaining to this Solution, please refer to them before installing.\n\nDemonstrates the CCF nested API polling pattern in Microsoft Sentinel. Includes a mock Contoso Incident API (Azure Function App) and a fully configured RestApiPoller connector that chains a list call to a detail call, ingesting complete incident records into the ContosoIncidents_CL table.\n\n**Data Connectors:** 1\n\n[Learn more about Microsoft Sentinel](https://aka.ms/azuresentinel) | [Learn more about Solutions](https://aka.ms/azuresentinelsolutionsdoc)", + "subscription": { + "resourceProviders": [ + "Microsoft.OperationsManagement/solutions", + "Microsoft.OperationalInsights/workspaces/providers/alertRules", + "Microsoft.Insights/workbooks", + "Microsoft.Logic/workflows" + ] + }, + "location": { + "metadata": { + "hidden": "Hiding location, we get it from the log analytics workspace" + }, + "visible": false + }, + "resourceGroup": { + "allowExisting": true + } + } + }, + "basics": [ + { + "name": "getLAWorkspace", + "type": "Microsoft.Solutions.ArmApiControl", + "toolTip": "This filters by workspaces that exist in the Resource Group selected", + "condition": "[greater(length(resourceGroup().name),0)]", + "request": { + "method": "GET", + "path": "[concat(subscription().id,'/providers/Microsoft.OperationalInsights/workspaces?api-version=2020-08-01')]" + } + }, + { + "name": "workspace", + "type": "Microsoft.Common.DropDown", + "label": "Workspace", + "placeholder": "Select a workspace", + "toolTip": "This dropdown will list only workspace that exists in the Resource Group selected", + "constraints": { + "allowedValues": "[map(filter(basics('getLAWorkspace').value, (filter) => contains(toLower(filter.id), toLower(resourceGroup().name))), (item) => parse(concat('{\"label\":\"', item.name, '\",\"value\":\"', item.name, '\"}')))]", + "required": true + }, + "visible": true + } + ], + "steps": [ + { + "name": "dataconnectors", + "label": "Data Connectors", + "bladeTitle": "Data Connectors", + "elements": [ + { + "name": "dataconnectors1-text", + "type": "Microsoft.Common.TextBlock", + "options": { + "text": "This Solution installs the data connector for Contoso Incidents (CCF Nested API Accelerator). You can get Contoso Incidents (CCF Nested API Accelerator) data in your Microsoft Sentinel workspace. After installing the solution, configure and enable this data connector by following guidance in Manage solution view." + } + }, + { + "name": "dataconnectors-link1", + "type": "Microsoft.Common.TextBlock", + "options": { + "link": { + "label": "Learn more about connecting data sources", + "uri": "https://docs.microsoft.com/azure/sentinel/connect-data-sources" + } + } + } + ] + } + ], + "outputs": { + "workspace-location": "[first(map(filter(basics('getLAWorkspace').value, (filter) => and(contains(toLower(filter.id), toLower(resourceGroup().name)),equals(filter.name,basics('workspace')))), (item) => item.location))]", + "location": "[location()]", + "workspace": "[basics('workspace')]" + } + } +} diff --git a/Tools/CCF-Pull-Connector-Nested-Accelerator/ContosoIncidents/Package/mainTemplate.json b/Tools/CCF-Pull-Connector-Nested-Accelerator/ContosoIncidents/Package/mainTemplate.json new file mode 100644 index 00000000000..f6813db79aa --- /dev/null +++ b/Tools/CCF-Pull-Connector-Nested-Accelerator/ContosoIncidents/Package/mainTemplate.json @@ -0,0 +1,703 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "author": "Microsoft", + "comments": "Solution template for ContosoIncidents" + }, + "parameters": { + "location": { + "type": "string", + "minLength": 1, + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Not used, but needed to pass arm-ttk test `Location-Should-Not-Be-Hardcoded`. We instead use the `workspace-location` which is derived from the LA workspace" + } + }, + "workspace-location": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "[concat('Region to deploy solution resources -- separate from location selection',parameters('location'))]" + } + }, + "workspace": { + "defaultValue": "", + "type": "string", + "metadata": { + "description": "Workspace name for Log Analytics where Microsoft Sentinel is setup" + } + }, + "resourceGroupName": { + "type": "string", + "defaultValue": "[resourceGroup().name]", + "metadata": { + "description": "resource group name where Microsoft Sentinel is setup" + } + }, + "subscription": { + "type": "string", + "defaultValue": "[last(split(subscription().id, '/'))]", + "metadata": { + "description": "subscription id where Microsoft Sentinel is setup" + } + } + }, + "variables": { + "_solutionName": "ContosoIncidents", + "_solutionVersion": "3.0.0", + "solutionId": "microsoft-sentinel.ccf-pull-connector-nested-accelerator", + "_solutionId": "[variables('solutionId')]", + "workspaceResourceId": "[resourceId('microsoft.OperationalInsights/Workspaces', parameters('workspace'))]", + "dataConnectorCCPVersion": "3.0.0", + "_dataConnectorContentIdConnectorDefinition1": "ContosoIncidentsConnector", + "dataConnectorTemplateNameConnectorDefinition1": "[concat(parameters('workspace'),'-dc-',uniquestring(variables('_dataConnectorContentIdConnectorDefinition1')))]", + "_dataConnectorContentIdConnections1": "ContosoIncidentsConnectorConnections", + "dataConnectorTemplateNameConnections1": "[concat(parameters('workspace'),'-dc-',uniquestring(variables('_dataConnectorContentIdConnections1')))]", + "dataCollectionEndpoint1": "[concat('/subscriptions/',parameters('subscription'),'/resourceGroups/',parameters('resourceGroupName'),'/providers/Microsoft.Insights/dataCollectionEndpoints/',parameters('workspace'))]", + "blanks": "[replace('b', 'b', '')]", + "_solutioncontentProductId": "[concat(take(variables('_solutionId'),50),'-','sl','-', uniqueString(concat(variables('_solutionId'),'-','Solution','-',variables('_solutionId'),'-', variables('_solutionVersion'))))]" + }, + "resources": [ + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/', variables('dataConnectorTemplateNameConnectorDefinition1'), variables('dataConnectorCCPVersion'))]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "contentId": "[variables('_dataConnectorContentIdConnectorDefinition1')]", + "displayName": "Contoso Incidents (CCF Nested API Accelerator)", + "contentKind": "DataConnector", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('dataConnectorCCPVersion')]", + "parameters": {}, + "variables": {}, + "resources": [ + { + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',variables('_dataConnectorContentIdConnectorDefinition1'))]", + "apiVersion": "2022-09-01-preview", + "type": "Microsoft.OperationalInsights/workspaces/providers/dataConnectorDefinitions", + "location": "[parameters('workspace-location')]", + "kind": "Customizable", + "properties": { + "connectorUiConfig": { + "id": "ContosoIncidentsConnector", + "title": "Contoso Incidents (CCF Nested API Accelerator)", + "publisher": "Contoso", + "descriptionMarkdown": "The **Contoso Incidents** connector demonstrates the CCF nested API polling pattern. It chains two REST API calls: a list endpoint that returns incident identifiers, followed by a detail endpoint called once per identifier. Deploy the included mock Contoso Incident API (Azure Function App) to observe the nested polling pattern end-to-end without a real data source.", + "graphQueriesTableName": "ContosoIncidents_CL", + "graphQueries": [ + { + "metricName": "Incidents ingested", + "legend": "ContosoIncidents_CL", + "baseQuery": "{{graphQueriesTableName}}" + } + ], + "sampleQueries": [ + { + "description": "All ingested incidents", + "query": "ContosoIncidents_CL\n| sort by TimeGenerated desc" + }, + { + "description": "High and Critical severity incidents", + "query": "ContosoIncidents_CL\n| where Severity in ('Critical', 'High')\n| sort by TimeGenerated desc" + }, + { + "description": "Active incidents", + "query": "ContosoIncidents_CL\n| where Status == 'Active'\n| project TimeGenerated, IncidentId, Title, Severity, AffectedUser, SourceIp" + } + ], + "dataTypes": [ + { + "name": "ContosoIncidents_CL", + "lastDataReceivedQuery": "ContosoIncidents_CL\n| summarize Time = max(TimeGenerated)\n| where isnotempty(Time)" + } + ], + "connectivityCriteria": [ + { + "type": "IsConnectedQuery", + "value": [ + "ContosoIncidents_CL\n| summarize LastLogReceived = max(TimeGenerated)\n| project IsConnected = LastLogReceived > ago(1d)" + ] + } + ], + "permissions": { + "resourceProvider": [ + { + "provider": "Microsoft.OperationalInsights/workspaces", + "permissionsDisplayText": "Read and write permissions on the Log Analytics workspace are required.", + "providerDisplayName": "Workspace", + "scope": "Workspace", + "requiredPermissions": { + "write": true, + "read": true, + "delete": true + } + } + ], + "customs": [ + { + "name": "Contoso Mock API", + "description": "The Contoso Mock API Function App must be deployed before configuring this connector. After deployment, retrieve the Function App API key to use in Step 2." + } + ] + }, + "instructionSteps": [ + { + "title": "STEP 1 — Deploy the Contoso Mock API", + "description": "Deploy the Azure Function App that serves as the mock Contoso Incident API:\n\n1. Deploy `MockApi/azuredeploy_MockApi.json` to a new resource group.\n2. Zip the `MockApi/` folder and run:\n```\naz functionapp deployment source config-zip --name --resource-group --src contosoapi.zip\n```\n3. Retrieve the Function App API key:\n```\naz functionapp keys list --name --resource-group --query functionKeys.default -o tsv\n```\n4. Note the `mockApiBaseUrl` deployment output (e.g. `https://contosomockapiXXXX.azurewebsites.net/api`).\n\nSee the [accelerator README](https://github.com/Azure/Azure-Sentinel/tree/master/Tools/CCF-Pull-Connector-Nested-Accelerator) for detailed steps or use GitHub Copilot agent mode with `agent-instructions.md` for fully automated deployment." + }, + { + "title": "STEP 2 — Connect to the Contoso Mock API", + "description": "Enter the Mock API base URL and Function App API key, then click **Connect**.", + "instructions": [ + { + "type": "Textbox", + "parameters": { + "label": "Mock API Base URL", + "placeholder": "https://contosomockapiXXXX.azurewebsites.net/api", + "type": "text", + "name": "mockApiBaseUrl" + } + }, + { + "type": "Textbox", + "parameters": { + "label": "Function App API Key", + "placeholder": "Enter your Function App API key", + "type": "password", + "name": "ApiKey" + } + }, + { + "type": "ConnectionToggleButton", + "parameters": { + "connectLabel": "connect", + "name": "connect" + } + } + ] + } + ] + } + } + }, + { + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('DataConnector-', variables('_dataConnectorContentIdConnectorDefinition1')))]", + "apiVersion": "2022-01-01-preview", + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "properties": { + "parentId": "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/dataConnectorDefinitions', variables('_dataConnectorContentIdConnectorDefinition1'))]", + "contentId": "[variables('_dataConnectorContentIdConnectorDefinition1')]", + "kind": "DataConnector", + "version": "[variables('dataConnectorCCPVersion')]", + "source": { + "sourceId": "[variables('_solutionId')]", + "name": "[variables('_solutionName')]", + "kind": "Solution" + }, + "author": { + "name": "Microsoft" + }, + "support": { + "name": "Microsoft Corporation", + "email": "azuresentinelpartner@microsoft.com", + "tier": "Microsoft", + "link": "https://learn.microsoft.com/azure/sentinel/create-codeless-connector" + }, + "dependencies": { + "criteria": [ + { + "version": "[variables('dataConnectorCCPVersion')]", + "contentId": "[variables('_dataConnectorContentIdConnections1')]", + "kind": "ResourcesDataConnector" + } + ] + } + } + }, + { + "name": "ContosoIncidents-DCR", + "apiVersion": "2022-06-01", + "type": "Microsoft.Insights/dataCollectionRules", + "location": "[parameters('workspace-location')]", + "kind": "[variables('blanks')]", + "properties": { + "dataCollectionEndpointId": "[variables('dataCollectionEndpoint1')]", + "streamDeclarations": { + "Custom-ContosoIncidents_CL": { + "columns": [ + { + "name": "TimeGenerated", + "type": "datetime" + }, + { + "name": "incidentId", + "type": "string" + }, + { + "name": "title", + "type": "string" + }, + { + "name": "severity", + "type": "string" + }, + { + "name": "status", + "type": "string" + }, + { + "name": "affectedUser", + "type": "string" + }, + { + "name": "sourceIp", + "type": "string" + } + ] + } + }, + "destinations": { + "logAnalytics": [ + { + "workspaceResourceId": "[variables('workspaceResourceId')]", + "name": "sentinel-workspace" + } + ] + }, + "dataFlows": [ + { + "streams": [ + "Custom-ContosoIncidents_CL" + ], + "destinations": [ + "sentinel-workspace" + ], + "transformKql": "source | extend TimeGenerated = now() | project TimeGenerated, IncidentId = ['incidentId'], Title = ['title'], Severity = ['severity'], Status = ['status'], AffectedUser = ['affectedUser'], SourceIp = ['sourceIp']", + "outputStream": "Custom-ContosoIncidents_CL" + } + ] + } + }, + { + "name": "ContosoIncidents_CL", + "apiVersion": "2022-10-01", + "type": "Microsoft.OperationalInsights/workspaces/tables", + "location": "[parameters('workspace-location')]", + "kind": null, + "properties": { + "plan": "Analytics", + "schema": { + "name": "ContosoIncidents_CL", + "columns": [ + { + "name": "TimeGenerated", + "type": "datetime", + "description": "Ingestion timestamp, set to the time the record was received by the data collection endpoint." + }, + { + "name": "IncidentId", + "type": "string", + "description": "Unique identifier of the incident (e.g. INC-001)." + }, + { + "name": "Title", + "type": "string", + "description": "Short description of the incident." + }, + { + "name": "Severity", + "type": "string", + "description": "Incident severity: Critical, High, Medium, or Low." + }, + { + "name": "Status", + "type": "string", + "description": "Current status of the incident: Active, Investigating, Resolved, or Closed." + }, + { + "name": "AffectedUser", + "type": "string", + "description": "UPN of the user associated with the incident." + }, + { + "name": "SourceIp", + "type": "string", + "description": "Source IP address associated with the incident." + } + ] + } + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "contentProductId": "[concat(take(variables('_solutionId'), 50),'-','dc','-', uniqueString(concat(variables('_solutionId'),'-','DataConnector','-',variables('_dataConnectorContentIdConnectorDefinition1'),'-', variables('dataConnectorCCPVersion'))))]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "version": "[variables('dataConnectorCCPVersion')]" + } + }, + { + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',variables('_dataConnectorContentIdConnectorDefinition1'))]", + "apiVersion": "2022-09-01-preview", + "type": "Microsoft.OperationalInsights/workspaces/providers/dataConnectorDefinitions", + "location": "[parameters('workspace-location')]", + "kind": "Customizable", + "properties": { + "connectorUiConfig": { + "id": "ContosoIncidentsConnector", + "title": "Contoso Incidents (CCF Nested API Accelerator)", + "publisher": "Contoso", + "descriptionMarkdown": "The **Contoso Incidents** connector demonstrates the CCF nested API polling pattern. It chains two REST API calls: a list endpoint that returns incident identifiers, followed by a detail endpoint called once per identifier. Deploy the included mock Contoso Incident API (Azure Function App) to observe the nested polling pattern end-to-end without a real data source.", + "graphQueriesTableName": "ContosoIncidents_CL", + "graphQueries": [ + { + "metricName": "Incidents ingested", + "legend": "ContosoIncidents_CL", + "baseQuery": "{{graphQueriesTableName}}" + } + ], + "sampleQueries": [ + { + "description": "All ingested incidents", + "query": "ContosoIncidents_CL\n| sort by TimeGenerated desc" + }, + { + "description": "High and Critical severity incidents", + "query": "ContosoIncidents_CL\n| where Severity in ('Critical', 'High')\n| sort by TimeGenerated desc" + }, + { + "description": "Active incidents", + "query": "ContosoIncidents_CL\n| where Status == 'Active'\n| project TimeGenerated, IncidentId, Title, Severity, AffectedUser, SourceIp" + } + ], + "dataTypes": [ + { + "name": "ContosoIncidents_CL", + "lastDataReceivedQuery": "ContosoIncidents_CL\n| summarize Time = max(TimeGenerated)\n| where isnotempty(Time)" + } + ], + "connectivityCriteria": [ + { + "type": "IsConnectedQuery", + "value": [ + "ContosoIncidents_CL\n| summarize LastLogReceived = max(TimeGenerated)\n| project IsConnected = LastLogReceived > ago(1d)" + ] + } + ], + "permissions": { + "resourceProvider": [ + { + "provider": "Microsoft.OperationalInsights/workspaces", + "permissionsDisplayText": "Read and write permissions on the Log Analytics workspace are required.", + "providerDisplayName": "Workspace", + "scope": "Workspace", + "requiredPermissions": { + "write": true, + "read": true, + "delete": true + } + } + ], + "customs": [ + { + "name": "Contoso Mock API", + "description": "The Contoso Mock API Function App must be deployed before configuring this connector. After deployment, retrieve the Function App API key to use in Step 2." + } + ] + }, + "instructionSteps": [ + { + "title": "STEP 1 — Deploy the Contoso Mock API", + "description": "Deploy the Azure Function App that serves as the mock Contoso Incident API:\n\n1. Deploy `MockApi/azuredeploy_MockApi.json` to a new resource group.\n2. Zip the `MockApi/` folder and run:\n```\naz functionapp deployment source config-zip --name --resource-group --src contosoapi.zip\n```\n3. Retrieve the Function App API key:\n```\naz functionapp keys list --name --resource-group --query functionKeys.default -o tsv\n```\n4. Note the `mockApiBaseUrl` deployment output (e.g. `https://contosomockapiXXXX.azurewebsites.net/api`).\n\nSee the [accelerator README](https://github.com/Azure/Azure-Sentinel/tree/master/Tools/CCF-Pull-Connector-Nested-Accelerator) for detailed steps or use GitHub Copilot agent mode with `agent-instructions.md` for fully automated deployment." + }, + { + "title": "STEP 2 — Connect to the Contoso Mock API", + "description": "Enter the Mock API base URL and Function App API key, then click **Connect**.", + "instructions": [ + { + "type": "Textbox", + "parameters": { + "label": "Mock API Base URL", + "placeholder": "https://contosomockapiXXXX.azurewebsites.net/api", + "type": "text", + "name": "mockApiBaseUrl" + } + }, + { + "type": "Textbox", + "parameters": { + "label": "Function App API Key", + "placeholder": "Enter your Function App API key", + "type": "password", + "name": "ApiKey" + } + }, + { + "type": "ConnectionToggleButton", + "parameters": { + "connectLabel": "connect", + "name": "connect" + } + } + ] + } + ] + } + } + }, + { + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('DataConnector-', variables('_dataConnectorContentIdConnectorDefinition1')))]", + "apiVersion": "2022-01-01-preview", + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "properties": { + "parentId": "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/dataConnectorDefinitions', variables('_dataConnectorContentIdConnectorDefinition1'))]", + "contentId": "[variables('_dataConnectorContentIdConnectorDefinition1')]", + "kind": "DataConnector", + "version": "[variables('dataConnectorCCPVersion')]", + "source": { + "sourceId": "[variables('_solutionId')]", + "name": "[variables('_solutionName')]", + "kind": "Solution" + }, + "author": { + "name": "Microsoft" + }, + "support": { + "name": "Microsoft Corporation", + "email": "azuresentinelpartner@microsoft.com", + "tier": "Microsoft", + "link": "https://learn.microsoft.com/azure/sentinel/create-codeless-connector" + }, + "dependencies": { + "criteria": [ + { + "version": "[variables('dataConnectorCCPVersion')]", + "contentId": "[variables('_dataConnectorContentIdConnections1')]", + "kind": "ResourcesDataConnector" + } + ] + } + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", + "apiVersion": "2023-04-01-preview", + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/', variables('dataConnectorTemplateNameConnections1'), variables('dataConnectorCCPVersion'))]", + "location": "[parameters('workspace-location')]", + "dependsOn": [ + "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" + ], + "properties": { + "contentId": "[variables('_dataConnectorContentIdConnections1')]", + "displayName": "Contoso Incidents (CCF Nested API Accelerator)", + "contentKind": "ResourcesDataConnector", + "mainTemplate": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "[variables('dataConnectorCCPVersion')]", + "parameters": { + "guidValue": { + "defaultValue": "[[newGuid()]", + "type": "securestring" + }, + "innerWorkspace": { + "defaultValue": "[parameters('workspace')]", + "type": "securestring" + }, + "connectorDefinitionName": { + "defaultValue": "Contoso Incidents (CCF Nested API Accelerator)", + "type": "securestring", + "minLength": 1 + }, + "workspace": { + "defaultValue": "[parameters('workspace')]", + "type": "securestring" + }, + "dcrConfig": { + "defaultValue": { + "dataCollectionEndpoint": "data collection Endpoint", + "dataCollectionRuleImmutableId": "data collection rule immutableId" + }, + "type": "object" + }, + "mockApiBaseUrl": { + "defaultValue": "mockApiBaseUrl", + "type": "securestring", + "minLength": 1 + }, + "ApiKey": { + "defaultValue": "ApiKey", + "type": "securestring", + "minLength": 1 + } + }, + "variables": { + "_dataConnectorContentIdConnections1": "[variables('_dataConnectorContentIdConnections1')]" + }, + "resources": [ + { + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('DataConnector-', variables('_dataConnectorContentIdConnections1')))]", + "apiVersion": "2022-01-01-preview", + "type": "Microsoft.OperationalInsights/workspaces/providers/metadata", + "properties": { + "parentId": "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/dataConnectors', variables('_dataConnectorContentIdConnections1'))]", + "contentId": "[variables('_dataConnectorContentIdConnections1')]", + "kind": "ResourcesDataConnector", + "version": "[variables('dataConnectorCCPVersion')]", + "source": { + "sourceId": "[variables('_solutionId')]", + "name": "[variables('_solutionName')]", + "kind": "Solution" + }, + "author": { + "name": "Microsoft" + }, + "support": { + "name": "Microsoft Corporation", + "email": "azuresentinelpartner@microsoft.com", + "tier": "Microsoft", + "link": "https://learn.microsoft.com/azure/sentinel/create-codeless-connector" + } + } + }, + { + "name": "[[concat(parameters('innerWorkspace'),'/Microsoft.SecurityInsights/', 'ContosoIncidents', parameters('guidValue'))]", + "apiVersion": "2023-02-01-preview", + "type": "Microsoft.OperationalInsights/workspaces/providers/dataConnectors", + "location": "[parameters('workspace-location')]", + "kind": "RestApiPoller", + "properties": { + "connectorDefinitionName": "ContosoIncidentsConnector", + "dataType": "ContosoIncidents_CL", + "dcrConfig": { + "streamName": "Custom-ContosoIncidents_CL", + "dataCollectionEndpoint": "[[parameters('dcrConfig').dataCollectionEndpoint]", + "dataCollectionRuleImmutableId": "[[parameters('dcrConfig').dataCollectionRuleImmutableId]" + }, + "auth": { + "type": "APIKey", + "ApiKey": "[[parameters('ApiKey')]", + "ApiKeyName": "x-functions-key" + }, + "request": { + "apiEndpoint": "[[concat(parameters('mockApiBaseUrl'),'/incidents')]", + "httpMethod": "GET", + "queryWindowInMin": 5, + "queryTimeFormat": "yyyy-MM-ddTHH:mm:ssZ", + "startTimeAttributeName": "startTime", + "endTimeAttributeName": "endTime", + "headers": { + "Accept": "application/json" + } + }, + "response": { + "eventsJsonPaths": [ + "$.incidents" + ], + "format": "json" + }, + "stepInfo": { + "stepType": "Nested", + "nextSteps": [ + { + "stepId": "fetchIncidentDetails", + "stepPlaceholdersParsingKql": "source | project res = parse_json(data) | project incidentId = res.incidentId" + } + ] + }, + "stepCollectorConfigs": { + "fetchIncidentDetails": { + "shouldJoinNestedData": false, + "request": { + "httpMethod": "GET", + "apiEndpoint": "[[concat(parameters('mockApiBaseUrl'),'/incidents/$incidentId$/details')]", + "headers": { + "Accept": "application/json" + } + }, + "response": { + "eventsJsonPaths": [ + "$" + ], + "format": "json" + } + } + } + } + } + ] + }, + "packageKind": "Solution", + "packageVersion": "[variables('_solutionVersion')]", + "packageName": "[variables('_solutionName')]", + "contentProductId": "[concat(take(variables('_solutionId'), 50),'-','rdc','-', uniqueString(concat(variables('_solutionId'),'-','ResourcesDataConnector','-',variables('_dataConnectorContentIdConnections1'),'-', variables('dataConnectorCCPVersion'))))]", + "packageId": "[variables('_solutionId')]", + "contentSchemaVersion": "3.0.0", + "version": "[variables('dataConnectorCCPVersion')]" + } + }, + { + "type": "Microsoft.OperationalInsights/workspaces/providers/contentPackages", + "apiVersion": "2023-04-01-preview", + "location": "[parameters('workspace-location')]", + "properties": { + "version": "3.0.0", + "kind": "Solution", + "contentSchemaVersion": "3.0.0", + "displayName": "ContosoIncidents", + "publisherDisplayName": "Microsoft Sentinel, Microsoft Corporation", + "descriptionHtml": "

Note: Please refer to the following before installing the solution:

\n

• Review the solution Release Notes

\n

• There may be known issues pertaining to this Solution, please refer to them before installing.

\n

Demonstrates the CCF nested API polling pattern in Microsoft Sentinel. Includes a mock Contoso Incident API (Azure Function App) and a fully configured RestApiPoller connector that chains a list call to a detail call, ingesting complete incident records into the ContosoIncidents_CL table.

\n

Data Connectors: 1

\n

Learn more about Microsoft Sentinel | Learn more about Solutions

\n", + "contentKind": "Solution", + "contentProductId": "[variables('_solutioncontentProductId')]", + "id": "[variables('_solutioncontentProductId')]", + "icon": "", + "contentId": "[variables('_solutionId')]", + "parentId": "[variables('_solutionId')]", + "source": { + "kind": "Solution", + "name": "ContosoIncidents", + "sourceId": "[variables('_solutionId')]" + }, + "author": { + "name": "Microsoft" + }, + "support": { + "name": "Microsoft Corporation", + "email": "azuresentinelpartner@microsoft.com", + "tier": "Microsoft", + "link": "https://learn.microsoft.com/azure/sentinel/create-codeless-connector" + }, + "dependencies": { + "operator": "AND", + "criteria": [ + { + "kind": "DataConnector", + "contentId": "[variables('_dataConnectorContentIdConnections1')]", + "version": "[variables('dataConnectorCCPVersion')]" + } + ] + }, + "firstPublishDate": "2026-06-05", + "lastPublishDate": "2026-06-05", + "providers": [ + "Contoso" + ], + "categories": { + "domains": [ + "Security - Threat Protection" + ] + } + }, + "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/', variables('_solutionId'))]" + } + ], + "outputs": {} +} diff --git a/Tools/CCF-Pull-Connector-Nested-Accelerator/ContosoIncidents/Package/testParameters.json b/Tools/CCF-Pull-Connector-Nested-Accelerator/ContosoIncidents/Package/testParameters.json new file mode 100644 index 00000000000..554801e41b7 --- /dev/null +++ b/Tools/CCF-Pull-Connector-Nested-Accelerator/ContosoIncidents/Package/testParameters.json @@ -0,0 +1,38 @@ +{ + "location": { + "type": "string", + "minLength": 1, + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Not used, but needed to pass arm-ttk test `Location-Should-Not-Be-Hardcoded`. We instead use the `workspace-location` which is derived from the LA workspace" + } + }, + "workspace-location": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "[concat('Region to deploy solution resources -- separate from location selection',parameters('location'))]" + } + }, + "workspace": { + "defaultValue": "", + "type": "string", + "metadata": { + "description": "Workspace name for Log Analytics where Microsoft Sentinel is setup" + } + }, + "resourceGroupName": { + "type": "string", + "defaultValue": "[resourceGroup().name]", + "metadata": { + "description": "resource group name where Microsoft Sentinel is setup" + } + }, + "subscription": { + "type": "string", + "defaultValue": "[last(split(subscription().id, '/'))]", + "metadata": { + "description": "subscription id where Microsoft Sentinel is setup" + } + } +} diff --git a/Tools/CCF-Pull-Connector-Nested-Accelerator/ContosoIncidents/ReleaseNotes.md b/Tools/CCF-Pull-Connector-Nested-Accelerator/ContosoIncidents/ReleaseNotes.md new file mode 100644 index 00000000000..44d5a53701f --- /dev/null +++ b/Tools/CCF-Pull-Connector-Nested-Accelerator/ContosoIncidents/ReleaseNotes.md @@ -0,0 +1,5 @@ +# Release Notes — Contoso Incidents CCF Nested API Accelerator + +| Version | Date | Notes | +|---|---|---| +| 3.0.0 | 05-06-2026 | Initial release. Mock API Function App with two HTTP endpoints (ListIncidents, GetIncidentDetails). CCF RestApiPoller with two-level nested polling. ContosoIncidents_CL table, DCR with transformKql, and connector UI definition. | diff --git a/Tools/CCF-Pull-Connector-Nested-Accelerator/ContosoIncidents/SolutionMetadata.json b/Tools/CCF-Pull-Connector-Nested-Accelerator/ContosoIncidents/SolutionMetadata.json new file mode 100644 index 00000000000..dfa205971a5 --- /dev/null +++ b/Tools/CCF-Pull-Connector-Nested-Accelerator/ContosoIncidents/SolutionMetadata.json @@ -0,0 +1,19 @@ +{ + "publisherId": "microsoft-sentinel", + "offerId": "ccf-pull-connector-nested-accelerator", + "firstPublishDate": "2026-06-05", + "lastPublishDate": "2026-06-05", + "url": "https://learn.microsoft.com/azure/sentinel/ccf-nested-api-polling", + "description": "Contoso Incidents — CCF Pull Connector Nested API Accelerator for Microsoft Sentinel. Demonstrates the nested API polling pattern with a live mock API and a fully working RestApiPoller connector.", + "support": { + "name": "Microsoft Corporation", + "email": "azuresentinelpartner@microsoft.com", + "tier": "Microsoft", + "link": "https://learn.microsoft.com/azure/sentinel/create-codeless-connector" + }, + "categories": { + "domains": [ "Security – Threat Protection" ] + }, + "providers": [ "Contoso" ], + "version": "1.0.0" +} diff --git a/Tools/CCF-Pull-Connector-Nested-Accelerator/MockApi/GetIncidentDetails/__init__.py b/Tools/CCF-Pull-Connector-Nested-Accelerator/MockApi/GetIncidentDetails/__init__.py new file mode 100644 index 00000000000..5529e439515 --- /dev/null +++ b/Tools/CCF-Pull-Connector-Nested-Accelerator/MockApi/GetIncidentDetails/__init__.py @@ -0,0 +1,108 @@ +import json +import logging +import azure.functions as func + + +# --------------------------------------------------------------------------- +# Mock incident data store. +# In a real API this data would come from a database. Here it is hardcoded +# so the demo works without any external dependencies. +# The shape of each record must match the DCR transformKql projection: +# IncidentId, Title, Severity, Status, createdAt, affectedUser, sourceIp +# --------------------------------------------------------------------------- +INCIDENTS: dict = { + "INC-001": { + "incidentId": "INC-001", + "title": "Suspicious login attempt", + "severity": "High", + "status": "Active", + "createdAt": "2026-05-30T14:22:00Z", + "affectedUser": "alice@contoso.com", + "sourceIp": "198.51.100.42", + }, + "INC-002": { + "incidentId": "INC-002", + "title": "Malware detection on endpoint", + "severity": "Critical", + "status": "Investigating", + "createdAt": "2026-05-30T15:07:00Z", + "affectedUser": "bob@contoso.com", + "sourceIp": "10.0.0.55", + }, + "INC-003": { + "incidentId": "INC-003", + "title": "Data exfiltration attempt", + "severity": "High", + "status": "Active", + "createdAt": "2026-05-30T16:44:00Z", + "affectedUser": "carol@contoso.com", + "sourceIp": "203.0.113.17", + }, + "INC-004": { + "incidentId": "INC-004", + "title": "Privilege escalation detected", + "severity": "Medium", + "status": "Resolved", + "createdAt": "2026-05-30T17:31:00Z", + "affectedUser": "dave@contoso.com", + "sourceIp": "192.168.1.88", + }, + "INC-005": { + "incidentId": "INC-005", + "title": "Port scan detected", + "severity": "Low", + "status": "Closed", + "createdAt": "2026-05-30T18:15:00Z", + "affectedUser": "eve@contoso.com", + "sourceIp": "172.16.0.22", + }, +} + + +def main(req: func.HttpRequest) -> func.HttpResponse: + """ + Mock Contoso Incident API — Get Incident Details endpoint. + + Mimics the second call in a nested API pattern. The CCF nested poller + extracts each incidentId from the ListIncidents response and calls this + endpoint once per ID to retrieve the full record. + + Route parameter: + incidentId The incident identifier returned by ListIncidents + (e.g. INC-001) + + Response shape (matched by eventsJsonPaths "$" in the CCF PollerConfig): + { + "incidentId": "INC-001", + "title": "...", + "severity": "...", + "status": "...", + "createdAt": "...", + "affectedUser": "...", + "sourceIp": "..." + } + """ + incident_id = req.route_params.get("incidentId", "") + + auth_header = req.headers.get("x-functions-key", "(missing)") + logging.info( + "ContosoMockApi GetIncidentDetails: incidentId=%s x-functions-key=%s", + incident_id, "(present)" if auth_header != "(missing)" else "(missing)", + ) + logging.info("ContosoMockApi GetIncidentDetails headers: %s", dict(req.headers)) + + if incident_id not in INCIDENTS: + logging.warning( + "ContosoMockApi GetIncidentDetails: unknown incidentId=%s", incident_id + ) + return func.HttpResponse( + body=json.dumps({"error": f"Incident '{incident_id}' not found."}), + mimetype="application/json", + status_code=404, + ) + + return func.HttpResponse( + body=json.dumps(INCIDENTS[incident_id]), + mimetype="application/json", + status_code=200, + ) diff --git a/Tools/CCF-Pull-Connector-Nested-Accelerator/MockApi/GetIncidentDetails/function.json b/Tools/CCF-Pull-Connector-Nested-Accelerator/MockApi/GetIncidentDetails/function.json new file mode 100644 index 00000000000..7b9d4508863 --- /dev/null +++ b/Tools/CCF-Pull-Connector-Nested-Accelerator/MockApi/GetIncidentDetails/function.json @@ -0,0 +1,18 @@ +{ + "scriptFile": "__init__.py", + "bindings": [ + { + "authLevel": "function", + "type": "httpTrigger", + "direction": "in", + "name": "req", + "methods": ["get"], + "route": "incidents/{incidentId}/details" + }, + { + "type": "http", + "direction": "out", + "name": "$return" + } + ] +} diff --git a/Tools/CCF-Pull-Connector-Nested-Accelerator/MockApi/ListIncidents/__init__.py b/Tools/CCF-Pull-Connector-Nested-Accelerator/MockApi/ListIncidents/__init__.py new file mode 100644 index 00000000000..8a6bd4a6927 --- /dev/null +++ b/Tools/CCF-Pull-Connector-Nested-Accelerator/MockApi/ListIncidents/__init__.py @@ -0,0 +1,53 @@ +import json +import logging +import azure.functions as func + + +def main(req: func.HttpRequest) -> func.HttpResponse: + """ + Mock Contoso Incident API — List Incidents endpoint. + + Mimics a real security product API that returns only a list of incident + identifiers for a given time window. The CCF nested poller calls this + endpoint first (call 1) and then calls GetIncidentDetails (call 2) once + per returned incidentId to retrieve the full record. + + Query parameters (accepted but not filtered on — all incidents are + returned on every call so the demo always produces visible data): + startTime ISO 8601 datetime + endTime ISO 8601 datetime + + Response shape (must match the eventsJsonPaths "$.incidents" in the + CCF PollerConfig): + { + "incidents": [ + { "incidentId": "INC-001" }, + ... + ] + } + """ + start_time = req.params.get("startTime", "(not provided)") + end_time = req.params.get("endTime", "(not provided)") + + auth_header = req.headers.get("x-functions-key", "(missing)") + logging.info( + "ContosoMockApi ListIncidents: startTime=%s endTime=%s x-functions-key=%s", + start_time, end_time, "(present)" if auth_header != "(missing)" else "(missing)", + ) + logging.info("ContosoMockApi ListIncidents headers: %s", dict(req.headers)) + + # Fixed set of five incidents — every poll returns all five so the + # demo produces data regardless of the query window. + incidents = [ + {"incidentId": "INC-001"}, + {"incidentId": "INC-002"}, + {"incidentId": "INC-003"}, + {"incidentId": "INC-004"}, + {"incidentId": "INC-005"}, + ] + + return func.HttpResponse( + body=json.dumps({"incidents": incidents}), + mimetype="application/json", + status_code=200, + ) diff --git a/Tools/CCF-Pull-Connector-Nested-Accelerator/MockApi/ListIncidents/function.json b/Tools/CCF-Pull-Connector-Nested-Accelerator/MockApi/ListIncidents/function.json new file mode 100644 index 00000000000..d1f62e599de --- /dev/null +++ b/Tools/CCF-Pull-Connector-Nested-Accelerator/MockApi/ListIncidents/function.json @@ -0,0 +1,18 @@ +{ + "scriptFile": "__init__.py", + "bindings": [ + { + "authLevel": "function", + "type": "httpTrigger", + "direction": "in", + "name": "req", + "methods": ["get"], + "route": "incidents" + }, + { + "type": "http", + "direction": "out", + "name": "$return" + } + ] +} diff --git a/Tools/CCF-Pull-Connector-Nested-Accelerator/MockApi/azuredeploy_MockApi.json b/Tools/CCF-Pull-Connector-Nested-Accelerator/MockApi/azuredeploy_MockApi.json new file mode 100644 index 00000000000..0d35f7bd3c8 --- /dev/null +++ b/Tools/CCF-Pull-Connector-Nested-Accelerator/MockApi/azuredeploy_MockApi.json @@ -0,0 +1,117 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "FunctionAppName": { + "type": "string", + "defaultValue": "ContosoMockApi", + "minLength": 3, + "maxLength": 24, + "metadata": { + "description": "Name prefix for the Function App and related resources. Must be globally unique — a random suffix is appended automatically." + } + }, + "Location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Azure region for all resources. Defaults to the resource group location." + } + } + }, + "variables": { + "uniqueSuffix": "[take(uniqueString(resourceGroup().id), 8)]", + "functionAppName": "[toLower(concat(parameters('FunctionAppName'), variables('uniqueSuffix')))]", + "storageAccountName": "[take(toLower(replace(variables('functionAppName'), '-', '')), 24)]", + "appInsightsName": "[concat(variables('functionAppName'), '-ai')]", + "storageSuffix": "[environment().suffixes.storage]" + }, + "resources": [ + { + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2022-09-01", + "name": "[variables('storageAccountName')]", + "location": "[parameters('Location')]", + "sku": { "name": "Standard_LRS" }, + "kind": "StorageV2", + "properties": { + "supportsHttpsTrafficOnly": true, + "minimumTlsVersion": "TLS1_2", + "allowBlobPublicAccess": false + } + }, + { + "type": "Microsoft.Insights/components", + "apiVersion": "2020-02-02", + "name": "[variables('appInsightsName')]", + "location": "[parameters('Location')]", + "kind": "web", + "properties": { + "Application_Type": "web", + "RetentionInDays": 30 + } + }, + { + "type": "Microsoft.Web/sites", + "apiVersion": "2022-03-01", + "name": "[variables('functionAppName')]", + "location": "[parameters('Location')]", + "kind": "functionapp,linux", + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]", + "[resourceId('Microsoft.Insights/components', variables('appInsightsName'))]" + ], + "properties": { + "httpsOnly": true, + "reserved": true, + "siteConfig": { + "linuxFxVersion": "python|3.11", + "appSettings": [ + { + "name": "FUNCTIONS_EXTENSION_VERSION", + "value": "~4" + }, + { + "name": "FUNCTIONS_WORKER_RUNTIME", + "value": "python" + }, + { + "name": "AzureWebJobsStorage", + "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2022-09-01').keys[0].value, ';EndpointSuffix=', variables('storageSuffix'))]" + }, + { + "name": "APPINSIGHTS_INSTRUMENTATIONKEY", + "value": "[reference(resourceId('Microsoft.Insights/components', variables('appInsightsName')), '2020-02-02').InstrumentationKey]" + }, + { + "name": "APPLICATIONINSIGHTS_CONNECTION_STRING", + "value": "[reference(resourceId('Microsoft.Insights/components', variables('appInsightsName')), '2020-02-02').ConnectionString]" + }, + { + "name": "SCM_DO_BUILD_DURING_DEPLOYMENT", + "value": "true" + } + ] + } + } + } + ], + "outputs": { + "functionAppName": { + "type": "string", + "value": "[variables('functionAppName')]" + }, + "mockApiBaseUrl": { + "type": "string", + "value": "[concat('https://', variables('functionAppName'), '.azurewebsites.net/api')]" + }, + "listIncidentsUrl": { + "type": "string", + "value": "[concat('https://', variables('functionAppName'), '.azurewebsites.net/api/incidents')]" + }, + "getIncidentDetailsUrl": { + "type": "string", + "value": "[concat('https://', variables('functionAppName'), '.azurewebsites.net/api/incidents/{incidentId}/details')]" + } + } +} diff --git a/Tools/CCF-Pull-Connector-Nested-Accelerator/MockApi/host.json b/Tools/CCF-Pull-Connector-Nested-Accelerator/MockApi/host.json new file mode 100644 index 00000000000..506924621b2 --- /dev/null +++ b/Tools/CCF-Pull-Connector-Nested-Accelerator/MockApi/host.json @@ -0,0 +1,15 @@ +{ + "version": "2.0", + "logging": { + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request" + } + } + }, + "extensionBundle": { + "id": "Microsoft.Azure.Functions.ExtensionBundle", + "version": "[3.*, 4.0.0)" + } +} diff --git a/Tools/CCF-Pull-Connector-Nested-Accelerator/MockApi/requirements.txt b/Tools/CCF-Pull-Connector-Nested-Accelerator/MockApi/requirements.txt new file mode 100644 index 00000000000..75db2c4f60f --- /dev/null +++ b/Tools/CCF-Pull-Connector-Nested-Accelerator/MockApi/requirements.txt @@ -0,0 +1 @@ +azure-functions diff --git a/Tools/CCF-Pull-Connector-Nested-Accelerator/README.md b/Tools/CCF-Pull-Connector-Nested-Accelerator/README.md new file mode 100644 index 00000000000..383057120e5 --- /dev/null +++ b/Tools/CCF-Pull-Connector-Nested-Accelerator/README.md @@ -0,0 +1,233 @@ +# CCF Pull Connector — Nested API Accelerator + +This accelerator demonstrates the **CCF nested API polling pattern** in Microsoft Sentinel end-to-end. It provides: + +- A **mock Contoso Incident API** (Azure Function App) with two HTTP endpoints +- A **CCF RestApiPoller connector** that chains the two calls using nested steps +- A fully working **`ContosoIncidents_CL`** table receiving enriched incident records + +Use this accelerator to learn how the CCF nested polling pattern works, test connector configurations, or use it as a starting template for your own multi-call REST API connectors. + +--- + +## GitHub Copilot Quick Deploy + +### Before You Start + +| Requirement | Details | +|---|---| +| **VS Code** | With the [GitHub Copilot](https://marketplace.visualstudio.com/items?itemName=GitHub.copilot-chat) extension installed and signed in | +| **Azure CLI** | Installed and logged in (`az login`). [Install guide](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli) | +| **Azure subscription** | With Contributor role on the subscription where you'll deploy | +| **This repo cloned locally** | Agent reads `agent-instructions.md` and deploys `ContosoIncidents/Package/mainTemplate.json` from disk | + +**What gets deployed:** + +- **Azure Function App** — the mock Contoso Incident API (list + detail endpoints) +- **Log Analytics workspace** — new or existing; Sentinel is enabled on it +- **Data Collection Endpoint and Rule** — DCE and DCR with stream declaration and KQL transform +- **`ContosoIncidents_CL` table** — custom Log Analytics table for the ingested records +- **CCF connector** — the `RestApiPoller` data connector and connector UI definition + +Paste the following into **GitHub Copilot Chat** in VS Code (Agent mode): + +``` +Load and follow the deployment instructions at Tools/CCF-Pull-Connector-Nested-Accelerator/agent-instructions.md. Let's deploy a CCF nested API connector. +``` + +The agent collects all required values, generating names you haven't specified, then deploys end-to-end and verifies data is flowing into `ContosoIncidents_CL`. The only manual step is clicking **Connect** once in the Sentinel portal. + +> Full agent instructions: [`agent-instructions.md`](./agent-instructions.md) + +--- + +## Architecture + +``` +Microsoft Sentinel (CCF RestApiPoller) + │ + │ 1. GET /incidents?startTime=...&endTime=... + ▼ +Contoso Mock API (Azure Function App) + │ → { "incidents": [{ "incidentId": "INC-001" }, ...] } + │ + │ 2. GET /incidents/INC-001/details + │ 2. GET /incidents/INC-002/details (one call per incidentId) + │ ... + ▼ +Contoso Mock API + │ → { "incidentId": "INC-001", "title": "...", "severity": "High", ... } + │ + ▼ +DCR transform → ContosoIncidents_CL table +``` + +The CCF engine extracts `incidentId` from each row returned by step 1 and substitutes it into the step 2 URL (`/incidents/$incidentId$/details`). The child response from each detail call is sent as a flat record to the DCR stream. The parent response is used only to extract the placeholder value. + +--- + +## Components + +| Component | Path | Description | +|---|---|---| +| Mock API Functions | `MockApi/` | Python Azure Functions serving the list and detail endpoints | +| Mock API ARM template | `MockApi/azuredeploy_MockApi.json` | Deploys Function App infrastructure (Storage, App Insights, Function App) | +| Table definition | `ContosoIncidents/Data Connectors/…/ContosoIncidents_Table.json` | Schema for `ContosoIncidents_CL` | +| DCR definition | `ContosoIncidents/Data Connectors/…/ContosoIncidents_DCR.json` | Stream declaration and transformKql | +| Poller config | `ContosoIncidents/Data Connectors/…/ContosoIncidents_PollerConfig.json` | RestApiPoller with nested step configuration | +| Connector UI | `ContosoIncidents/Data Connectors/…/ContosoIncidents_ConnectorDefinition.json` | Sentinel data connector UI definition | +| Solution template | `ContosoIncidents/Package/mainTemplate.json` | ARM template: deploys DCE, table, DCR, connector definition, and poller | + +--- + +## Mock API Endpoints + +The mock API returns static data and requires API key authentication via the `x-functions-key` request header. The key is retrieved automatically during deployment. + +| Method | URL | Description | +|---|---|---| +| `GET` | `/api/incidents?startTime={t}&endTime={t}` | Returns a list of 5 incident identifiers | +| `GET` | `/api/incidents/{incidentId}/details` | Returns the full record for a given incident | + +**Sample list response:** +```json +{ + "incidents": [ + { "incidentId": "INC-001" }, + { "incidentId": "INC-002" }, + { "incidentId": "INC-003" }, + { "incidentId": "INC-004" }, + { "incidentId": "INC-005" } + ] +} +``` + +**Sample detail response:** +```json +{ + "incidentId": "INC-001", + "title": "Suspicious login attempt", + "severity": "High", + "status": "Active", + "affectedUser": "alice@contoso.com", + "sourceIp": "198.51.100.42", + "createdAt": "2026-05-30T14:22:00Z" +} +``` + +--- + +## Output Table Schema — `ContosoIncidents_CL` + +| Column | Type | Source | +|---|---|---| +| `TimeGenerated` | datetime | Ingestion time, set to `now()` by the DCR transform | +| `IncidentId` | string | `incidentId` from the API response | +| `Title` | string | `title` from the API response | +| `Severity` | string | `severity` from the API response. Values: Critical, High, Medium, Low | +| `Status` | string | `status` from the API response. Values: Active, Investigating, Resolved, Closed | +| `AffectedUser` | string | `affectedUser` from the API response | +| `SourceIp` | string | `sourceIp` from the API response | + +--- + +## Manual Deployment + +### Step 1 — Deploy the Mock API + +Deploy the Function App infrastructure: + +```powershell +az group create \ + --name contoso-mock-api-rg \ + --location eastus + +az deployment group create \ + --resource-group contoso-mock-api-rg \ + --template-file "Tools/CCF-Pull-Connector-Nested-Accelerator/MockApi/azuredeploy_MockApi.json" \ + --parameters FunctionAppName=ContosoMockApi \ + --output table +``` + +Note the `mockApiBaseUrl` and `functionAppName` from the deployment output. + +### Step 2 — Deploy the Mock API Code + +Zip the `MockApi/` folder and deploy it: + +```powershell +# From the workspace root +Compress-Archive -Path "Tools/CCF-Pull-Connector-Nested-Accelerator/MockApi/*" -DestinationPath contosoapi.zip -Force + +az functionapp deployment source config-zip ` + --name ` + --resource-group contoso-mock-api-rg ` + --src contosoapi.zip +``` + +Verify the endpoints respond: + +```powershell +$base = "" +Invoke-RestMethod "$base/incidents" | ConvertTo-Json +Invoke-RestMethod "$base/incidents/INC-001/details" | ConvertTo-Json +``` + +### Step 3 — Deploy the Sentinel Connector + +```powershell +# Write parameters file (the hyphen in workspace-location causes az CLI issues when passed inline) +@{ + '$schema' = 'https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#' + contentVersion = '1.0.0.0' + parameters = @{ + workspace = @{ value = '' } + 'workspace-location' = @{ value = '' } + } +} | ConvertTo-Json -Depth 5 | Out-File deploy-params.json -Encoding utf8 + +az deployment group create ` + --resource-group ` + --template-file "Tools/CCF-Pull-Connector-Nested-Accelerator/ContosoIncidents/Package/mainTemplate.json" ` + --parameters "@deploy-params.json" ` + --output table +``` + +### Step 4 — Verify Data + +After clicking **Connect** in the Sentinel portal (Step 4 in the connector UI), allow 5–15 minutes for the first poll cycle, then query in Sentinel Logs: + +```kql +ContosoIncidents_CL +| sort by TimeGenerated desc +| take 10 +``` + +You should see 5 rows, one per mock incident, with all columns populated. + +--- + +## Troubleshooting + +| Symptom | Likely Cause | Fix | +|---|---|---| +| `ContosoIncidents_CL` table not found | ARM deployment failed or table not created | Re-run `mainTemplate.json` and check `provisioningState` | +| No rows after 15 minutes | Mock API not reachable | Test endpoints manually with `Invoke-RestMethod`; check Function App is running | +| All rows show `TimeGenerated = now()` | Expected: the DCR transform sets `TimeGenerated = now()` for all records | No action needed | +| ARM deployment fails on `dataConnectors` resource | Connector definition not yet provisioned | Re-run the template. This is a transient race condition. | +| Function App returns 404 for `/api/incidents` | Code not deployed or route not registered | Re-run the zip deploy step; confirm `SCM_DO_BUILD_DURING_DEPLOYMENT=true` in app settings | +| `workspace-location` mismatch error | Parameter passed with wrong region | Must exactly match `az monitor log-analytics workspace show --query location` | + +--- + +## Security Notes + +The mock API uses **Azure Functions API key authentication** (`x-functions-key` header). The key is retrieved at deploy time and passed to the connector as a parameter; the CCF poller sends it with every request. For production connectors, configure the appropriate `auth` block in the `RestApiPoller` (API Key, OAuth2, Basic, etc.) with matching `authLevel` on your Function App or API endpoint. + +--- + +## Related + +- [CCF Blob Connector Accelerator](../CCF-Blob-Connector-Accelerator/) +- [Nested API polling reference](https://learn.microsoft.com/azure/sentinel/ccf-nested-api-polling) +- [Create a codeless connector](https://learn.microsoft.com/azure/sentinel/create-codeless-connector) diff --git a/Tools/CCF-Pull-Connector-Nested-Accelerator/agent-instructions.md b/Tools/CCF-Pull-Connector-Nested-Accelerator/agent-instructions.md new file mode 100644 index 00000000000..0d8f2577883 --- /dev/null +++ b/Tools/CCF-Pull-Connector-Nested-Accelerator/agent-instructions.md @@ -0,0 +1,393 @@ +# CCF Pull Connector — Nested API Accelerator: Agent Deployment Instructions + +> These are instructions for a GitHub Copilot agent. When a user pastes the trigger prompt, +> load this file and follow the steps below exactly. + +--- + +## Step 0 — Collect Deployment Values + +**Start here every time.** Before taking any deployment action, work through the values below. + +**Default behaviour when a value is missing**: this is a lab accelerator, so resource names do not need to be meaningful as long as they are unique in the environment. **If the user doesn't provide a value, generate one yourself** (e.g. append a random 4-digit suffix like `contoso-nested-ws-4821`). Do not ask for values the user hasn't volunteered. Generate sensible defaults, show them in the confirmation table, and proceed. + +The only value you must confirm with the user is the **Subscription ID** (if more than one exists). + +Collect all values and print the confirmation table before executing any `az` command. + +--- + +### 1. Azure Subscription ID + +Run to see available subscriptions and let the user choose: + +```powershell +az account list --query "[].{name:name, id:id, isDefault:isDefault}" -o table +``` + +If only one subscription exists (or one is marked `isDefault: True`), confirm it with the user rather than assuming. + +--- + +### 2. Sentinel Workspace Name + Resource Group + +If not provided, ask: *"Do you have an existing Sentinel workspace, or should I create a new one?"* + +**Existing workspace** — list and let the user pick: + +```powershell +az monitor log-analytics workspace list ` + --query "[].{name:name, resourceGroup:resourceGroup, location:location}" -o table +``` + +Derive `workspace-resource-group` and `location` from the chosen row — do not ask again. + +**New workspace** — suggest these names and ask the user to confirm or change them: +- Workspace name: `contoso-nested-ws` +- Resource group: `contoso-nested-rg` +- Location: see value 3 below + +Once confirmed, run: + +```powershell +# Create resource group (skip if reusing an existing one) +az group create ` + --name ` + --location ` + --output table + +# Create the Log Analytics workspace +az monitor log-analytics workspace create ` + --workspace-name ` + --resource-group ` + --location ` + --output table + +# Enable Microsoft Sentinel on the workspace +# Note: 'az security insights create' does not exist — use az rest instead +$WS_ID = (az monitor log-analytics workspace show ` + --name ` + --resource-group ` + --query id -o tsv).Trim() + +az rest --method PUT ` + --url "https://management.azure.com$WS_ID/providers/Microsoft.SecurityInsights/onboardingStates/default?api-version=2024-03-01" ` + --body '{}' +``` + +**Verify**: confirm workspace exists and Sentinel is enabled: + +```powershell +az monitor log-analytics workspace show ` + --name ` + --resource-group ` + --query "{name:name, location:location, resourceGroup:resourceGroup, provisioningState:provisioningState}" ` + --output table +``` + +--- + +### 3. Azure Region / Location + +- If reusing an **existing** workspace, inherit its `location` automatically — do **not** ask. +- If creating a **new** workspace, suggest `eastus` and ask the user to confirm. +- This value is passed as `workspace-location` in Step 3. It must match the workspace region exactly. + +--- + +### 4. Mock API Resource Group + +- Suggest `contoso-mock-api-rg` (separate from the Sentinel workspace RG). +- Ask the user to confirm or provide a different name. +- The Mock API can share the workspace RG or use a dedicated one — both work. + +--- + +### 5. Mock API Function App Name Prefix + +- Suggest `ContosoMockApi` as the prefix. +- A random 8-character suffix is appended automatically by the ARM template to ensure global uniqueness. +- Ask the user to confirm or change the prefix. + +--- + +### Value Summary Checkpoint + +Once all values are collected, print a confirmation table before taking any action: + +``` +Subscription ID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx +Workspace name: my-sentinel-ws +Workspace RG: sentinel-rg +Region / location: eastus +Mock API RG: contoso-mock-api-rg +Function App prefix: ContosoMockApi +``` + +Ask: **"Does this look correct? Type `yes` to begin deployment."** + +--- + +## Deployment Rules — Follow Without Exception + +1. **Never** use the VS Code `deploy_connector` extension tool — always deploy using + `az deployment group create` or `az functionapp deployment source config-zip`. + +2. **Always** pass `workspace-location=` explicitly in the Step 3 CLI command. + Omitting it causes `workspace-location` to default to an empty string, which causes + DCR and connector definition resource placement to fail. + +3. To enable Sentinel on a new workspace use `az rest`, **not** `az security insights create` + (that command does not exist). + +4. Step 5 (clicking **Connect** in the portal) is the **only manual action**. Every other step is CLI. The ARM deployment in Step 4 registers the connector definition but does NOT start polling. Only the Connect button starts data collection. + +5. The Function App name is `<8-char-suffix>` (lowercase, no hyphens). Always read + the `functionAppName` output from the Step 1 deployment to get the actual name before + proceeding to Step 2. + +6. After **every** CLI step, automatically verify success before proceeding. Run a follow-up + `az` command confirming `provisioningState=Succeeded` or the expected resource state. + Report the result inline, then immediately continue. Only pause and ask when a step has + genuinely failed and you cannot self-recover. + +7. This is a dev/demo accelerator. The Function App API key and Mock API URL **should be displayed clearly in chat** at Step 5 so the user can copy them directly into the portal without switching to the terminal. + +--- + +## Step 1 — Deploy the Mock API Infrastructure + +```powershell +# Create resource group +az group create ` + --name ` + --location ` + --output table + +# Deploy Function App infrastructure +cd # e.g. C:\GitHub\Azure-Sentinel +az deployment group create ` + --resource-group ` + --template-file "Tools/CCF-Pull-Connector-Nested-Accelerator/MockApi/azuredeploy_MockApi.json" ` + --parameters FunctionAppName= Location= ` + --output table +``` + +**Capture outputs** and store for use in later steps: + +```powershell +$outputs = (az deployment group show ` + --resource-group ` + --name azuredeploy_MockApi ` + --query properties.outputs -o json | ConvertFrom-Json) + +$functionAppName = $outputs.functionAppName.value +$mockApiBaseUrl = $outputs.mockApiBaseUrl.value + +Write-Host "Function App: $functionAppName" +Write-Host "Base URL: $mockApiBaseUrl" +``` + +**Verify**: confirm Function App is in Running state: + +```powershell +az functionapp show ` + --name $functionAppName ` + --resource-group ` + --query "{name:name, state:state, location:location}" ` + --output table +``` + +--- + +## Step 2 — Deploy the Mock API Code + +Zip the `MockApi/` folder contents and deploy to the Function App: + +```powershell +# Run from workspace root +$zipPath = Join-Path $PWD "contosoapi.zip" +Compress-Archive ` + -Path "Tools/CCF-Pull-Connector-Nested-Accelerator/MockApi/*" ` + -DestinationPath $zipPath ` + -Force + +az functionapp deployment source config-zip ` + --name $functionAppName ` + --resource-group ` + --src $zipPath ` + --output table +``` + +**Verify** the Function App is running (allow 60 seconds for cold start): + +```powershell +az functionapp show ` + --name $functionAppName ` + --resource-group ` + --query "{name:name, state:state}" ` + --output table +``` + +> **Note**: Full endpoint verification (with API key) runs in Step 3 after the key is retrieved. + +--- + +## Step 3 — Retrieve the Function App API Key + +Retrieve the default function key. Display it clearly in chat so the user can copy it directly into the portal in Step 5. + +```powershell +$apiKey = (az functionapp keys list ` + --name $functionAppName ` + --resource-group ` + --query functionKeys.default -o tsv).Trim() + +Write-Host "API key: $apiKey" +``` + +Display the key clearly in chat so the user can copy it directly into the portal in Step 5. + +**Verify** the endpoints respond correctly: + +```powershell +# List endpoint — expect 5 incidents +$listResponse = Invoke-RestMethod "$mockApiBaseUrl/incidents" -Headers @{"x-functions-key" = $apiKey} +Write-Host "Incidents returned: $($listResponse.incidents.Count)" + +# Detail endpoint — expect INC-001 details +$detailResponse = Invoke-RestMethod "$mockApiBaseUrl/incidents/INC-001/details" -Headers @{"x-functions-key" = $apiKey} +Write-Host "INC-001 title: $($detailResponse.title)" +``` + +--- + +## Step 4 — Deploy the Sentinel Solution + +Deploy the main template which creates the DCE, `ContosoIncidents_CL` table, DCR, and connector definition. + +> **Note on `workspace-location`**: The hyphen in this parameter name causes the az CLI to reject it if passed inline. Write a parameters file first, then pass it with `@`. + +```powershell +# Write parameters file (avoids hyphen parsing issue with workspace-location) +$params = @{ + "\$schema" = "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#" + "contentVersion" = "1.0.0.0" + "parameters" = @{ + "workspace" = @{ "value" = "" } + "workspace-location" = @{ "value" = "" } + } +} | ConvertTo-Json -Depth 5 +$params | Out-File -FilePath "deploy-params.json" -Encoding utf8 + +# Deploy +az deployment group create ` + --resource-group ` + --name mainTemplate ` + --template-file "Tools/CCF-Pull-Connector-Nested-Accelerator/ContosoIncidents/Package/mainTemplate.json" ` + --parameters "@deploy-params.json" ` + --output table +``` + +**Verify**: confirm deployment succeeded: + +```powershell +az deployment group show ` + --resource-group ` + --name mainTemplate ` + --query "{state:properties.provisioningState, timestamp:properties.timestamp}" ` + --output table +``` + +Once confirmed, print the full deployment summary so the user has a single reference for all resources: + +```powershell +$subId = (az account show --query id -o tsv).Trim() + +Write-Host "" +Write-Host "========== Deployment Summary ==========" +Write-Host "" +Write-Host ("Subscription ID : " + $subId) +Write-Host ("Region : ") +Write-Host "" +Write-Host "--- Sentinel Workspace ---" +Write-Host ("Workspace Name : ") +Write-Host ("Workspace RG : ") +Write-Host "" +Write-Host "--- Mock API ---" +Write-Host ("Function App Name : " + $functionAppName) +Write-Host ("Resource Group : ") +Write-Host ("Mock API Base URL : " + $mockApiBaseUrl) +Write-Host ("API Key : (run: Write-Host `$apiKey to copy)") +Write-Host "" +Write-Host "========================================" +``` + +--- + +## Step 5 — Enable the Connector (Portal) + +The ARM deployment from Step 4 registers the connector definition — it does **not** start data collection. The user must click **Connect** in the portal once, providing the Mock API URL and API key. This is the only manual action in the entire deployment. + +Print the connection values the user needs to paste into the portal **directly in chat** — display both values clearly: + +| Field | Value | +|-------|-------| +| Mock API Base URL | *(print actual value of `$mockApiBaseUrl`)* | +| Function App API Key | *(print actual value of `$apiKey`)* | + +Guide the user to: + +1. Navigate to **Microsoft Sentinel** → **Data Connectors** +2. Find **Contoso Incidents (CCF Nested API Accelerator)** — if not visible, click **Refresh** or wait 2–3 minutes for the ARM deployment to propagate +3. Click **Open connector page** +4. Under **STEP 2 — Connect to the Contoso Mock API**, enter: + + | Field | Value | + |-------|-------| + | Mock API Base URL | `` (from Step 1 output) | + | Function App API Key | *(run `Write-Host $apiKey` in terminal and paste the value)* | + +5. Click **connect** + +Once clicked, the CCF engine begins polling immediately. No further manual steps are required. + +--- + +## Step 6 — Verify Data in Log Analytics + +Allow **5–10 minutes** for the first poll cycle after clicking Connect, then confirm data arrived. + +Guide the user to **Microsoft Sentinel → Logs** to run: + +```kql +ContosoIncidents_CL +| sort by TimeGenerated desc +| take 10 +``` + +Expected result: **5 rows** with columns `IncidentId`, `Title`, `Severity`, `Status`, `AffectedUser`, `SourceIp` all populated. + +- `TimeGenerated` = ingestion time (`now()`) for all records — the DCR transform sets this unconditionally. +- Connector status in the Data Connectors blade should show **Connected**. + +If no rows appear after 15 minutes: +1. Confirm the connector status is **Connected** (not **Disconnected**) in the portal +2. Re-check the API key is correct by testing the endpoint manually +3. Check the DCR: `az monitor data-collection rule show --name ContosoIncidents-DCR --resource-group ` + +--- + +## Cleanup (Optional) + +To remove all deployed resources, ask the user to confirm before running destructive commands: + +```powershell +# Remove Mock API Function App and its resource group +az group delete --name --yes --no-wait + +# Remove Sentinel resources (if workspace RG is dedicated to this accelerator) +az group delete --name --yes --no-wait +``` + +> If the workspace RG contains other resources, delete individual resources via the portal rather than the entire RG. \ No newline at end of file