From 22e3e6e9034c4210ca459222a3637233f80fb6e6 Mon Sep 17 00:00:00 2001 From: v-rucdu Date: Thu, 26 May 2022 10:55:44 +0530 Subject: [PATCH] Solution Tool Updates for Template Spec Migration (#4655) * Initial Template Spec Automation * Example Template Spec Input File * Updated code to add Template Spec for parser * Updated Dataconnector meatdata id * Handled Template Spec for AR, HQ and Workbooks * 1PConnector support and techniques, id prop for HQ * Handled the review scenarios * Updated Package tool for comments from Sarath * Tool updates * Updated files * Working Template with Analytical Rule Fix * Updated ResourceId ref of Workbook, AR and HQ * Fixed the solutionId issue * Fixed AnalyticalRule typo * Fixing query frequency, query period issue * Updated code as per Roey's feedback * Incorporated the feedback from Roey * Changed ParserName * Modified Template Spec Name * Added missing status property for Analytics Rule * Workbook Metadata and Analytic Rules Changes * Update createSolution.ps1 * Update createSolution.ps1 * Fixed multiple workbook key issue * Reverted parser updates * Commiting changes for the workbooks and contentId fix * Checking-in the Parser changes for template specs * Changing the function alias of the parser object * Content Types are referenced as varaibles across metadata dependencies and changed Parser content id * Update createSolution.ps1 * Template Spec V2 Tooling Changes * upated analytical rule version to 2.0.0 * read the version property from input file * Copied code to the V2 folder * Handled UIdefinition changes in templating file * Deleted unwanted files * Deleted unwanted files * Removed preview keyword * IsPreview flag for data connector has been handled * Workbook UI Parameter Block commented * Removing workbook name from UI * Versioning change for the content types * Added the logic for the existing function apps title * Function App existing code modified Logic * adding the description validation check * Workbook Versioning change * ISV email property handling in the tool * Playbook TemplateSpec code changes * Updated correct content for Playbooks * Fixed JSON Validation issues * Added missing metadata prop * Added new template spec name code changes * Update Metadata Path * Added resource property for DC content changes * Added customConnectorCount, Removed Junk Resource * Fixed the locale issue in documentation links * Added ReadMe file and Resolve review comments (#5115) * Added ReadMe file and Resolve review comments * Fixed PR validation issue Co-authored-by: Eli Forbes Co-authored-by: v-sabiraj Co-authored-by: Sarath Tirumalareddy Co-authored-by: Sapan Goel <95875056+ms-sapangoel@users.noreply.github.com> Co-authored-by: ashishsyal <89064706+ashishsyal@users.noreply.github.com> --- .../SolutionMetadata.json | 16 + .../azuredeploy.json | 13 +- .../azuredeploy.json | 13 +- .../azuredeploy.json | 13 +- .../azuredeploy.json | 13 +- .../azuredeploy.json | 37 +- .../azuredeploy.json | 37 +- .../azuredeploy.json | 37 +- .../azuredeploy.json | 37 +- Solutions/CiscoUmbrella/SolutionMetadata.json | 16 + .../OktaCustomConnector/azuredeploy.json | 5 - .../Create-Azure-Sentinel-Solution/README.md | 18 +- .../V2/README.md | 363 +++ .../V2/createSolutionV2.ps1 | 2396 +++++++++++++++++ .../Solution_CiscoUmbrellaTemplateSpec.json | 48 + .../V2/templating/SolutionAutomationInput.ts | 34 + .../V2/templating/baseCreateUiDefinition.json | 60 + .../V2/templating/baseMainTemplate.json | 36 + .../V2/templating/replaceLocationValue.js | 13 + .../templating/replacePlaybookParamNames.js | 13 + .../V2/templating/replacePlaybookVarNames.js | 13 + .../templating/SolutionAutomationInput.ts | 3 + .../templating/baseMainTemplate.json | 2 +- .../src/Send-AzMonitorCustomLogs.ps1 | 80 +- 24 files changed, 3230 insertions(+), 86 deletions(-) create mode 100644 Solutions/AkamaiSecurityEvents/SolutionMetadata.json create mode 100644 Solutions/CiscoUmbrella/SolutionMetadata.json create mode 100644 Tools/Create-Azure-Sentinel-Solution/V2/README.md create mode 100644 Tools/Create-Azure-Sentinel-Solution/V2/createSolutionV2.ps1 create mode 100644 Tools/Create-Azure-Sentinel-Solution/V2/input/Solution_CiscoUmbrellaTemplateSpec.json create mode 100644 Tools/Create-Azure-Sentinel-Solution/V2/templating/SolutionAutomationInput.ts create mode 100644 Tools/Create-Azure-Sentinel-Solution/V2/templating/baseCreateUiDefinition.json create mode 100644 Tools/Create-Azure-Sentinel-Solution/V2/templating/baseMainTemplate.json create mode 100644 Tools/Create-Azure-Sentinel-Solution/V2/templating/replaceLocationValue.js create mode 100644 Tools/Create-Azure-Sentinel-Solution/V2/templating/replacePlaybookParamNames.js create mode 100644 Tools/Create-Azure-Sentinel-Solution/V2/templating/replacePlaybookVarNames.js diff --git a/Solutions/AkamaiSecurityEvents/SolutionMetadata.json b/Solutions/AkamaiSecurityEvents/SolutionMetadata.json new file mode 100644 index 0000000000..ad74f658b1 --- /dev/null +++ b/Solutions/AkamaiSecurityEvents/SolutionMetadata.json @@ -0,0 +1,16 @@ +{ + "publisherId": "azuresentinel", + "offerId": "azure-sentinel-solution-akamai", + "firstPublishDate": "2022-03-2", + "providers": [ "Microsoft" ], + "categories": { + "domains": [ "Security - Cloud Security" ], + "verticals": [] + }, + "support": { + "tier": "Microsoft", + "name": "Microsoft Corporation", + "email": "support@microsoft.com", + "link": "https://support.microsoft.com/" + } +} \ No newline at end of file diff --git a/Solutions/CiscoUmbrella/Playbooks/CiscoUmbrellaEnforcementAPIConnector/azuredeploy.json b/Solutions/CiscoUmbrella/Playbooks/CiscoUmbrellaEnforcementAPIConnector/azuredeploy.json index ed9b723bb2..d70acf847a 100644 --- a/Solutions/CiscoUmbrella/Playbooks/CiscoUmbrellaEnforcementAPIConnector/azuredeploy.json +++ b/Solutions/CiscoUmbrella/Playbooks/CiscoUmbrellaEnforcementAPIConnector/azuredeploy.json @@ -1,15 +1,18 @@ { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", - "parameters": {}, - "variables": { - "customApis_CiscoUmbrellaEnforcementAPI_name": "CiscoUmbrellaEnforcementAPI" + "parameters": { + "customApis_CiscoUmbrellaEnforcementAPI_name": { + "defaultValue": "CiscoUmbrellaEnforcementAPI", + "type": "String" + } }, + "variables": {}, "resources": [ { "type": "Microsoft.Web/customApis", "apiVersion": "2016-06-01", - "name": "[variables('customApis_CiscoUmbrellaEnforcementAPI_name')]", + "name": "[parameters('customApis_CiscoUmbrellaEnforcementAPI_name')]", "location": "[resourceGroup().location]", "properties": { "connectionParameters": { @@ -29,7 +32,7 @@ }, "brandColor": "#FFFFFF", "description": "Connector for Cisco Umbrella Enforcment API", - "displayName": "[variables('customApis_CiscoUmbrellaEnforcementAPI_name')]", + "displayName": "[parameters('customApis_CiscoUmbrellaEnforcementAPI_name')]", "iconUri": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAMAAABiM0N1AAABdFBMVEUAAAAA//8AgP8Aqv8AgL8AmcwAmeYAotEAldUAn98AntsAodkAnNYAn9cIotgIntoHoNsHnNwHodcHndgGotoGoNsGnNYGn9cFn9sEoNgEndkEoNoEntsEntgEoNsEn9gDn9kDoNoDn9gDoNgDn9kFn9gFntgFn9kFn9oFoNoFn9gEn9kEoNkEn9oEoNgEn9kEntkEoNoEoNkEn9kEn9kEntkEoNgEntgEn9kEn9oEntoEn9gEoNgEntkEn9kEoNkEn9oEn9oEntgDn9kDn9gDoNkDn9kFn9oFn9gFn9kFntkFn9oEn9kEntoEn9gEoNkEn9kEoNkEn9oEn9gEn9kEn9kEn9kEn9kEn9kEn9kEn9kEn9kEn9kEntkEn9kEn9kEn9kEn9oDn9kFn9kEoNkEn9kEn9kEn9kEn9kEn9oEn9kEn9kEn9kEn9kEoNkEn9kEn9gEn9kEn9kEn9kEn9kEn9kEoNkEn9kEn9kEn9kEn9n///940Z2XAAAAenRSTlMAAQIDBAUKCwwQFRsfICEiIyQmJykrLC04Ozw+P0JGSEpTVVtdaGlqbW5wcnN1dnh5e35/gIGDhIeIiYqLjI2Oj5CRkpibnKSlp6mqsLGys7q7v8DCxMfKz9DS1NXW19jZ2t7g4+To6u3u7/Dx8vP09fb3+Pn6+/z9/h6/P2sAAAABYktHRHtP0rX8AAACB0lEQVRYw+2WaVPTYBRGb9SK4oI7dd+tAiqCra1FqKIoKu4bUFQQtJY2SUna2vPr/VCSafKmpONkHMfJ86m5OXOmN3Pfm4jEiRMnTtQZKadCkfWRHkTrlMKRcg8iIALkvxRlKsM+yq10RTLVW6qohuETuZWuSA1zi7+r/uiKBLb4z4jyxt0wUd5Ih4jyRlps7DCRjRUisrG26khpoOsdIDqR5bRmUvPhTsVFan7EKdjYMq5n2zdHK/5H6lRcJF31IU4hZ2TjNfJHolL4Qi71tLMv/xyOAInzV3LgftE0vjz1zMnRZz/qa28GAgg5PrtmrTw8onqG6oBvPQzqAJxQCe1eC4C68jExBKroOWA2NkUe4rZzwVWvZ78NzGeuZJc6RWW4qCUG+xXicBMac4X3gLXXI5oCHu0QkW2dogpcCCSmYeOkiKSACY+oCN93KofyHbReJgOIJWi/e17DgkdkwJx6uk83AWa2K4QJZ9rLE99rXQ8UyblvADcUwoCzIiJyB6pKa30B+yZx/iMsK0QR8iIi2gdY9IgKwEzC/7BFRPp+saEQD8A6JSLXgYJ3rG1g/mZq4mun6NKx/j2jsKIQh5rQfJF/CzQO9jCQm6VJlRh3B3LMP9rX7G6ihd0qoeXaR6SV05TDtm9y0dQ/z3aKXi1X9U9ju4IIST5ZtVYfJ+NlFydOnKjyG0KNPhWovN8UAAAAAElFTkSuQmCC", "backendService": { "serviceUrl": "https://s-platform.api.opendns.com" diff --git a/Solutions/CiscoUmbrella/Playbooks/CiscoUmbrellaInvestigateAPIConnector/azuredeploy.json b/Solutions/CiscoUmbrella/Playbooks/CiscoUmbrellaInvestigateAPIConnector/azuredeploy.json index bad95a28d7..00e68ab344 100644 --- a/Solutions/CiscoUmbrella/Playbooks/CiscoUmbrellaInvestigateAPIConnector/azuredeploy.json +++ b/Solutions/CiscoUmbrella/Playbooks/CiscoUmbrellaInvestigateAPIConnector/azuredeploy.json @@ -1,15 +1,18 @@ { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", - "parameters": {}, - "variables": { - "customApis_CiscoUmbrellaInvestigateAPIConnector_name": "CiscoUmbrellaInvestigateAPI" + "parameters": { + "customApis_CiscoUmbrellaInvestigateAPIConnector_name": { + "defaultValue": "CiscoUmbrellaInvestigateAPI", + "type": "String" + } }, + "variables": {}, "resources": [ { "type": "Microsoft.Web/customApis", "apiVersion": "2016-06-01", - "name": "[variables('customApis_CiscoUmbrellaInvestigateAPIConnector_name')]", + "name": "[parameters('customApis_CiscoUmbrellaInvestigateAPIConnector_name')]", "location": "[resourceGroup().location]", "properties": { "connectionParameters": { @@ -29,7 +32,7 @@ }, "brandColor": "#FFFFFF", "description": "Connector for Cisco Umbrella Investigate API", - "displayName": "[variables('customApis_CiscoUmbrellaInvestigateAPIConnector_name')]", + "displayName": "[parameters('customApis_CiscoUmbrellaInvestigateAPIConnector_name')]", "iconUri": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAMAAABiM0N1AAABdFBMVEUAAAAA//8AgP8Aqv8AgL8AmcwAmeYAotEAldUAn98AntsAodkAnNYAn9cIotgIntoHoNsHnNwHodcHndgGotoGoNsGnNYGn9cFn9sEoNgEndkEoNoEntsEntgEoNsEn9gDn9kDoNoDn9gDoNgDn9kFn9gFntgFn9kFn9oFoNoFn9gEn9kEoNkEn9oEoNgEn9kEntkEoNoEoNkEn9kEn9kEntkEoNgEntgEn9kEn9oEntoEn9gEoNgEntkEn9kEoNkEn9oEn9oEntgDn9kDn9gDoNkDn9kFn9oFn9gFn9kFntkFn9oEn9kEntoEn9gEoNkEn9kEoNkEn9oEn9gEn9kEn9kEn9kEn9kEn9kEn9kEn9kEn9kEn9kEntkEn9kEn9kEn9kEn9oDn9kFn9kEoNkEn9kEn9kEn9kEn9kEn9oEn9kEn9kEn9kEn9kEoNkEn9kEn9gEn9kEn9kEn9kEn9kEn9kEoNkEn9kEn9kEn9kEn9n///940Z2XAAAAenRSTlMAAQIDBAUKCwwQFRsfICEiIyQmJykrLC04Ozw+P0JGSEpTVVtdaGlqbW5wcnN1dnh5e35/gIGDhIeIiYqLjI2Oj5CRkpibnKSlp6mqsLGys7q7v8DCxMfKz9DS1NXW19jZ2t7g4+To6u3u7/Dx8vP09fb3+Pn6+/z9/h6/P2sAAAABYktHRHtP0rX8AAACB0lEQVRYw+2WaVPTYBRGb9SK4oI7dd+tAiqCra1FqKIoKu4bUFQQtJY2SUna2vPr/VCSafKmpONkHMfJ86m5OXOmN3Pfm4jEiRMnTtQZKadCkfWRHkTrlMKRcg8iIALkvxRlKsM+yq10RTLVW6qohuETuZWuSA1zi7+r/uiKBLb4z4jyxt0wUd5Ih4jyRlps7DCRjRUisrG26khpoOsdIDqR5bRmUvPhTsVFan7EKdjYMq5n2zdHK/5H6lRcJF31IU4hZ2TjNfJHolL4Qi71tLMv/xyOAInzV3LgftE0vjz1zMnRZz/qa28GAgg5PrtmrTw8onqG6oBvPQzqAJxQCe1eC4C68jExBKroOWA2NkUe4rZzwVWvZ78NzGeuZJc6RWW4qCUG+xXicBMac4X3gLXXI5oCHu0QkW2dogpcCCSmYeOkiKSACY+oCN93KofyHbReJgOIJWi/e17DgkdkwJx6uk83AWa2K4QJZ9rLE99rXQ8UyblvADcUwoCzIiJyB6pKa30B+yZx/iMsK0QR8iIi2gdY9IgKwEzC/7BFRPp+saEQD8A6JSLXgYJ3rG1g/mZq4mun6NKx/j2jsKIQh5rQfJF/CzQO9jCQm6VJlRh3B3LMP9rX7G6ihd0qoeXaR6SV05TDtm9y0dQ/z3aKXi1X9U9ju4IIST5ZtVYfJ+NlFydOnKjyG0KNPhWovN8UAAAAAElFTkSuQmCC", "backendService": { "serviceUrl": "https://investigate.api.umbrella.com" diff --git a/Solutions/CiscoUmbrella/Playbooks/CiscoUmbrellaManagementAPIConnector/azuredeploy.json b/Solutions/CiscoUmbrella/Playbooks/CiscoUmbrellaManagementAPIConnector/azuredeploy.json index 0944cf4227..a5ebb0e766 100644 --- a/Solutions/CiscoUmbrella/Playbooks/CiscoUmbrellaManagementAPIConnector/azuredeploy.json +++ b/Solutions/CiscoUmbrella/Playbooks/CiscoUmbrellaManagementAPIConnector/azuredeploy.json @@ -1,15 +1,18 @@ { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", - "parameters": {}, - "variables": { - "customApis_CiscoUmbrellaManagementAPI_name": "CiscoUmbrellaManagementAPI" + "parameters": { + "customApis_CiscoUmbrellaManagementAPI_name": { + "defaultValue": "CiscoUmbrellaManagementAPI", + "type": "String" + } }, + "variables": {}, "resources": [ { "type": "Microsoft.Web/customApis", "apiVersion": "2016-06-01", - "name": "[variables('customApis_CiscoUmbrellaManagementAPI_name')]", + "name": "[parameters('customApis_CiscoUmbrellaManagementAPI_name')]", "location": "[resourceGroup().location]", "properties": { "connectionParameters": { @@ -42,7 +45,7 @@ }, "brandColor": "#FFFFFF", "description": "Connector for Cisco Umbrella Management API", - "displayName": "[variables('customApis_CiscoUmbrellaManagementAPI_name')]", + "displayName": "[parameters('customApis_CiscoUmbrellaManagementAPI_name')]", "iconUri": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAMAAABiM0N1AAABdFBMVEUAAAAA//8AgP8Aqv8AgL8AmcwAmeYAotEAldUAn98AntsAodkAnNYAn9cIotgIntoHoNsHnNwHodcHndgGotoGoNsGnNYGn9cFn9sEoNgEndkEoNoEntsEntgEoNsEn9gDn9kDoNoDn9gDoNgDn9kFn9gFntgFn9kFn9oFoNoFn9gEn9kEoNkEn9oEoNgEn9kEntkEoNoEoNkEn9kEn9kEntkEoNgEntgEn9kEn9oEntoEn9gEoNgEntkEn9kEoNkEn9oEn9oEntgDn9kDn9gDoNkDn9kFn9oFn9gFn9kFntkFn9oEn9kEntoEn9gEoNkEn9kEoNkEn9oEn9gEn9kEn9kEn9kEn9kEn9kEn9kEn9kEn9kEn9kEntkEn9kEn9kEn9kEn9oDn9kFn9kEoNkEn9kEn9kEn9kEn9kEn9oEn9kEn9kEn9kEn9kEoNkEn9kEn9gEn9kEn9kEn9kEn9kEn9kEoNkEn9kEn9kEn9kEn9n///940Z2XAAAAenRSTlMAAQIDBAUKCwwQFRsfICEiIyQmJykrLC04Ozw+P0JGSEpTVVtdaGlqbW5wcnN1dnh5e35/gIGDhIeIiYqLjI2Oj5CRkpibnKSlp6mqsLGys7q7v8DCxMfKz9DS1NXW19jZ2t7g4+To6u3u7/Dx8vP09fb3+Pn6+/z9/h6/P2sAAAABYktHRHtP0rX8AAACB0lEQVRYw+2WaVPTYBRGb9SK4oI7dd+tAiqCra1FqKIoKu4bUFQQtJY2SUna2vPr/VCSafKmpONkHMfJ86m5OXOmN3Pfm4jEiRMnTtQZKadCkfWRHkTrlMKRcg8iIALkvxRlKsM+yq10RTLVW6qohuETuZWuSA1zi7+r/uiKBLb4z4jyxt0wUd5Ih4jyRlps7DCRjRUisrG26khpoOsdIDqR5bRmUvPhTsVFan7EKdjYMq5n2zdHK/5H6lRcJF31IU4hZ2TjNfJHolL4Qi71tLMv/xyOAInzV3LgftE0vjz1zMnRZz/qa28GAgg5PrtmrTw8onqG6oBvPQzqAJxQCe1eC4C68jExBKroOWA2NkUe4rZzwVWvZ78NzGeuZJc6RWW4qCUG+xXicBMac4X3gLXXI5oCHu0QkW2dogpcCCSmYeOkiKSACY+oCN93KofyHbReJgOIJWi/e17DgkdkwJx6uk83AWa2K4QJZ9rLE99rXQ8UyblvADcUwoCzIiJyB6pKa30B+yZx/iMsK0QR8iIi2gdY9IgKwEzC/7BFRPp+saEQD8A6JSLXgYJ3rG1g/mZq4mun6NKx/j2jsKIQh5rQfJF/CzQO9jCQm6VJlRh3B3LMP9rX7G6ihd0qoeXaR6SV05TDtm9y0dQ/z3aKXi1X9U9ju4IIST5ZtVYfJ+NlFydOnKjyG0KNPhWovN8UAAAAAElFTkSuQmCC", "backendService": { "serviceUrl": "https://management.api.umbrella.com" diff --git a/Solutions/CiscoUmbrella/Playbooks/CiscoUmbrellaNetworkDeviceManagementAPIConnector/azuredeploy.json b/Solutions/CiscoUmbrella/Playbooks/CiscoUmbrellaNetworkDeviceManagementAPIConnector/azuredeploy.json index d2e3e164c9..c8a9af3357 100644 --- a/Solutions/CiscoUmbrella/Playbooks/CiscoUmbrellaNetworkDeviceManagementAPIConnector/azuredeploy.json +++ b/Solutions/CiscoUmbrella/Playbooks/CiscoUmbrellaNetworkDeviceManagementAPIConnector/azuredeploy.json @@ -1,15 +1,18 @@ { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", - "parameters": {}, - "variables": { - "customApis_CiscoUmbrellaNetworkDeviceManagementAPI_name": "CiscoUmbrellaNetworkDeviceManagementAPI" + "parameters": { + "customApis_CiscoUmbrellaNetworkDeviceManagementAPI_name": { + "defaultValue": "CiscoUmbrellaNetworkDeviceManagementAPI", + "type": "String" + } }, + "variables": {}, "resources": [ { "type": "Microsoft.Web/customApis", "apiVersion": "2016-06-01", - "name": "[variables('customApis_CiscoUmbrellaNetworkDeviceManagementAPI_name')]", + "name": "[parameters('customApis_CiscoUmbrellaNetworkDeviceManagementAPI_name')]", "location": "[resourceGroup().location]", "properties": { "connectionParameters": { @@ -42,7 +45,7 @@ }, "brandColor": "#FFFFFF", "description": "Connector for Cisco Umbrella Network Device Management API", - "displayName": "[variables('customApis_CiscoUmbrellaNetworkDeviceManagementAPI_name')]", + "displayName": "[parameters('customApis_CiscoUmbrellaNetworkDeviceManagementAPI_name')]", "iconUri": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAMAAABiM0N1AAABdFBMVEUAAAAA//8AgP8Aqv8AgL8AmcwAmeYAotEAldUAn98AntsAodkAnNYAn9cIotgIntoHoNsHnNwHodcHndgGotoGoNsGnNYGn9cFn9sEoNgEndkEoNoEntsEntgEoNsEn9gDn9kDoNoDn9gDoNgDn9kFn9gFntgFn9kFn9oFoNoFn9gEn9kEoNkEn9oEoNgEn9kEntkEoNoEoNkEn9kEn9kEntkEoNgEntgEn9kEn9oEntoEn9gEoNgEntkEn9kEoNkEn9oEn9oEntgDn9kDn9gDoNkDn9kFn9oFn9gFn9kFntkFn9oEn9kEntoEn9gEoNkEn9kEoNkEn9oEn9gEn9kEn9kEn9kEn9kEn9kEn9kEn9kEn9kEn9kEntkEn9kEn9kEn9kEn9oDn9kFn9kEoNkEn9kEn9kEn9kEn9kEn9oEn9kEn9kEn9kEn9kEoNkEn9kEn9gEn9kEn9kEn9kEn9kEn9kEoNkEn9kEn9kEn9kEn9n///940Z2XAAAAenRSTlMAAQIDBAUKCwwQFRsfICEiIyQmJykrLC04Ozw+P0JGSEpTVVtdaGlqbW5wcnN1dnh5e35/gIGDhIeIiYqLjI2Oj5CRkpibnKSlp6mqsLGys7q7v8DCxMfKz9DS1NXW19jZ2t7g4+To6u3u7/Dx8vP09fb3+Pn6+/z9/h6/P2sAAAABYktHRHtP0rX8AAACB0lEQVRYw+2WaVPTYBRGb9SK4oI7dd+tAiqCra1FqKIoKu4bUFQQtJY2SUna2vPr/VCSafKmpONkHMfJ86m5OXOmN3Pfm4jEiRMnTtQZKadCkfWRHkTrlMKRcg8iIALkvxRlKsM+yq10RTLVW6qohuETuZWuSA1zi7+r/uiKBLb4z4jyxt0wUd5Ih4jyRlps7DCRjRUisrG26khpoOsdIDqR5bRmUvPhTsVFan7EKdjYMq5n2zdHK/5H6lRcJF31IU4hZ2TjNfJHolL4Qi71tLMv/xyOAInzV3LgftE0vjz1zMnRZz/qa28GAgg5PrtmrTw8onqG6oBvPQzqAJxQCe1eC4C68jExBKroOWA2NkUe4rZzwVWvZ78NzGeuZJc6RWW4qCUG+xXicBMac4X3gLXXI5oCHu0QkW2dogpcCCSmYeOkiKSACY+oCN93KofyHbReJgOIJWi/e17DgkdkwJx6uk83AWa2K4QJZ9rLE99rXQ8UyblvADcUwoCzIiJyB6pKa30B+yZx/iMsK0QR8iIi2gdY9IgKwEzC/7BFRPp+saEQD8A6JSLXgYJ3rG1g/mZq4mun6NKx/j2jsKIQh5rQfJF/CzQO9jCQm6VJlRh3B3LMP9rX7G6ihd0qoeXaR6SV05TDtm9y0dQ/z3aKXi1X9U9ju4IIST5ZtVYfJ+NlFydOnKjyG0KNPhWovN8UAAAAAElFTkSuQmCC", "backendService": { "serviceUrl": "https://management.api.umbrella.com" diff --git a/Solutions/CiscoUmbrella/Playbooks/Playbooks/CiscoUmbrella-AddIpToDestinationList/azuredeploy.json b/Solutions/CiscoUmbrella/Playbooks/Playbooks/CiscoUmbrella-AddIpToDestinationList/azuredeploy.json index 6cac5b5b1c..9d663974f7 100644 --- a/Solutions/CiscoUmbrella/Playbooks/Playbooks/CiscoUmbrella-AddIpToDestinationList/azuredeploy.json +++ b/Solutions/CiscoUmbrella/Playbooks/Playbooks/CiscoUmbrella-AddIpToDestinationList/azuredeploy.json @@ -1,6 +1,32 @@ { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", + "metadata": { + "title": "CiscoUmbrella-AddIpToDestinationList", + "description": "This playbook showcases an example of triggering an incident within a targeted Teams channel and opening up a ticket within Service Now. Additionally The playbook will also list playbooks that can be initiated from teams using an adaptive card and callbacks that will take action upon certain entities identified in the incident.", + "prerequisites": [ + "1. ServiceNow Instance URL, Username, and password.", + "2. Access and authorization to enable API connectors", + "3. Teams Group ID and Alert Channel ID where the messages are to be posted in." + ], + "lastUpdateTime": "2021-06-29T10:00:00.000Z", + "entities": [ + "Account", + "Url", + "Host" + ], + "tags": [ + "Sync", + "Notification", + "Teams Response" + ], + "support": { + "tier": "community" + }, + "author": { + "name": "Jing Nghik" + } + }, "parameters": { "PlaybookName": { "defaultValue": "CiscoUmbrella-AddIpToDestinationList", @@ -26,13 +52,16 @@ "metadata": { "description": "Id of the Teams Channel where the adaptive card will be posted." } + }, + "customApis_ciscoumbrellamanagement_name": { + "defaultValue": "CiscoUmbrellaManagementAPI", + "type": "String" } }, "variables": { "AzureSentinelConnectionName": "[concat('azuresentinel-', parameters('PlaybookName'))]", "TeamsConnectionName": "[concat('teams-', parameters('PlaybookName'))]", - "CiscoUmbrellaManagementAPIConnectionName": "[concat('ciscoumbrellamanagement-connection-', parameters('PlaybookName'))]", - "customApis_ciscoumbrellamanagement_name": "CiscoUmbrellaManagementAPI" + "CiscoUmbrellaManagementAPIConnectionName": "[concat('ciscoumbrellamanagement-connection-', parameters('PlaybookName'))]" }, "resources": [ { @@ -58,7 +87,7 @@ "displayName": "[variables('CiscoUmbrellaManagementAPIConnectionName')]", "customParameterValues": {}, "api": { - "id": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', resourceGroup().name, '/providers/Microsoft.Web/customApis/', variables('customApis_ciscoumbrellamanagement_name'))]" + "id": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', resourceGroup().name, '/providers/Microsoft.Web/customApis/', parameters('customApis_ciscoumbrellamanagement_name'))]" } } }, @@ -1120,7 +1149,7 @@ "ciscoumbrellamanagement": { "connectionId": "[resourceId('Microsoft.Web/connections', variables('CiscoUmbrellaManagementAPIConnectionName'))]", "connectionName": "[variables('CiscoUmbrellaManagementAPIConnectionName')]", - "id": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', resourceGroup().name, '/providers/Microsoft.Web/customApis/', variables('customApis_ciscoumbrellamanagement_name'))]" + "id": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', resourceGroup().name, '/providers/Microsoft.Web/customApis/', parameters('customApis_ciscoumbrellamanagement_name'))]" } } } diff --git a/Solutions/CiscoUmbrella/Playbooks/Playbooks/CiscoUmbrella-AssignPolicyToIdentity/azuredeploy.json b/Solutions/CiscoUmbrella/Playbooks/Playbooks/CiscoUmbrella-AssignPolicyToIdentity/azuredeploy.json index db03eb66ee..b80302a8b1 100644 --- a/Solutions/CiscoUmbrella/Playbooks/Playbooks/CiscoUmbrella-AssignPolicyToIdentity/azuredeploy.json +++ b/Solutions/CiscoUmbrella/Playbooks/Playbooks/CiscoUmbrella-AssignPolicyToIdentity/azuredeploy.json @@ -1,6 +1,32 @@ { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", + "metadata": { + "title": "CiscoUmbrella-AssignPolicyToIdentity", + "description": "This playbook showcases an example of triggering an incident within a targeted Teams channel and opening up a ticket within Service Now. Additionally The playbook will also list playbooks that can be initiated from teams using an adaptive card and callbacks that will take action upon certain entities identified in the incident.", + "prerequisites": [ + "1. ServiceNow Instance URL, Username, and password.", + "2. Access and authorization to enable API connectors", + "3. Teams Group ID and Alert Channel ID where the messages are to be posted in." + ], + "lastUpdateTime": "2021-06-29T10:00:00.000Z", + "entities": [ + "Account", + "Url", + "Host" + ], + "tags": [ + "Sync", + "Notification", + "Teams Response" + ], + "support": { + "tier": "community" + }, + "author": { + "name": "Jing Nghik" + } + }, "parameters": { "PlaybookName": { "defaultValue": "CiscoUmbrella-AssignPolicyToIdentity", @@ -9,12 +35,15 @@ "PolicyId": { "defaultValue": "", "type": "String" + }, + "customApis_ciscoumbrellanetworkdevicemanagement_name": { + "defaultValue": "CiscoUmbrellaNetworkDeviceManagementAPI", + "type": "String" } }, "variables": { "AzureSentinelConnectionName": "[concat('azuresentinel-', parameters('PlaybookName'))]", - "CiscoUmbrellaNetworkDeviceManagementAPIConnectionName": "[concat('ciscoumbrellanetworkdevice-connection-', parameters('PlaybookName'))]", - "customApis_ciscoumbrellanetworkdevicemanagement_name": "CiscoUmbrellaNetworkDeviceManagementAPI" + "CiscoUmbrellaNetworkDeviceManagementAPIConnectionName": "[concat('ciscoumbrellanetworkdevice-connection-', parameters('PlaybookName'))]" }, "resources": [ { @@ -40,7 +69,7 @@ "displayName": "[variables('CiscoUmbrellaNetworkDeviceManagementAPIConnectionName')]", "customParameterValues": {}, "api": { - "id": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', resourceGroup().name, '/providers/Microsoft.Web/customApis/', variables('customApis_ciscoumbrellanetworkdevicemanagement_name'))]" + "id": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', resourceGroup().name, '/providers/Microsoft.Web/customApis/', parameters('customApis_ciscoumbrellanetworkdevicemanagement_name'))]" } } }, @@ -385,7 +414,7 @@ "ciscoumbrellanetworkdevicemanagement": { "connectionId": "[resourceId('Microsoft.Web/connections', variables('CiscoUmbrellaNetworkDeviceManagementAPIConnectionName'))]", "connectionName": "[variables('CiscoUmbrellaNetworkDeviceManagementAPIConnectionName')]", - "id": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', resourceGroup().name, '/providers/Microsoft.Web/customApis/', variables('customApis_ciscoumbrellanetworkdevicemanagement_name'))]" + "id": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', resourceGroup().name, '/providers/Microsoft.Web/customApis/', parameters('customApis_ciscoumbrellanetworkdevicemanagement_name'))]" } } } diff --git a/Solutions/CiscoUmbrella/Playbooks/Playbooks/CiscoUmbrella-BlockDomain/azuredeploy.json b/Solutions/CiscoUmbrella/Playbooks/Playbooks/CiscoUmbrella-BlockDomain/azuredeploy.json index 2ac77dd47e..85c0e8ae6d 100644 --- a/Solutions/CiscoUmbrella/Playbooks/Playbooks/CiscoUmbrella-BlockDomain/azuredeploy.json +++ b/Solutions/CiscoUmbrella/Playbooks/Playbooks/CiscoUmbrella-BlockDomain/azuredeploy.json @@ -1,16 +1,45 @@ { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", + "metadata": { + "title": "CiscoUmbrella-BlockDomain", + "description": "This playbook showcases an example of triggering an incident within a targeted Teams channel and opening up a ticket within Service Now. Additionally The playbook will also list playbooks that can be initiated from teams using an adaptive card and callbacks that will take action upon certain entities identified in the incident.", + "prerequisites": [ + "1. ServiceNow Instance URL, Username, and password.", + "2. Access and authorization to enable API connectors", + "3. Teams Group ID and Alert Channel ID where the messages are to be posted in." + ], + "lastUpdateTime": "2021-06-29T10:00:00.000Z", + "entities": [ + "Account", + "Url", + "Host" + ], + "tags": [ + "Sync", + "Notification", + "Teams Response" + ], + "support": { + "tier": "community" + }, + "author": { + "name": "Jing Nghik" + } + }, "parameters": { "PlaybookName": { "defaultValue": "CiscoUmbrella-BlockDomain", "type": "String" + }, + "customApis_ciscoumbrellaenforcement_name": { + "defaultValue": "CiscoUmbrellaEnforcementAPI", + "type": "String" } }, "variables": { "AzureSentinelConnectionName": "[concat('azuresentinel-', parameters('PlaybookName'))]", - "CiscoUmbrellaEnforcementAPIConnectionName": "[concat('ciscoumbrellaenforcement-connection-', parameters('PlaybookName'))]", - "customApis_ciscoumbrellaenforcement_name": "CiscoUmbrellaEnforcementAPI" + "CiscoUmbrellaEnforcementAPIConnectionName": "[concat('ciscoumbrellaenforcement-connection-', parameters('PlaybookName'))]" }, "resources": [ { @@ -36,7 +65,7 @@ "displayName": "[variables('CiscoUmbrellaEnforcementAPIConnectionName')]", "customParameterValues": {}, "api": { - "id": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', resourceGroup().name, '/providers/Microsoft.Web/customApis/', variables('customApis_ciscoumbrellaenforcement_name'))]" + "id": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', resourceGroup().name, '/providers/Microsoft.Web/customApis/', parameters('customApis_ciscoumbrellaenforcement_name'))]" } } }, @@ -229,7 +258,7 @@ "ciscoumbrellaenforcement": { "connectionId": "[resourceId('Microsoft.Web/connections', variables('CiscoUmbrellaEnforcementAPIConnectionName'))]", "connectionName": "[variables('CiscoUmbrellaEnforcementAPIConnectionName')]", - "id": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', resourceGroup().name, '/providers/Microsoft.Web/customApis/', variables('customApis_ciscoumbrellaenforcement_name'))]" + "id": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', resourceGroup().name, '/providers/Microsoft.Web/customApis/', parameters('customApis_ciscoumbrellaenforcement_name'))]" } } } diff --git a/Solutions/CiscoUmbrella/Playbooks/Playbooks/CiscoUmbrella-GetDomainInfo/azuredeploy.json b/Solutions/CiscoUmbrella/Playbooks/Playbooks/CiscoUmbrella-GetDomainInfo/azuredeploy.json index 8d5799ab44..bbbd7d5921 100644 --- a/Solutions/CiscoUmbrella/Playbooks/Playbooks/CiscoUmbrella-GetDomainInfo/azuredeploy.json +++ b/Solutions/CiscoUmbrella/Playbooks/Playbooks/CiscoUmbrella-GetDomainInfo/azuredeploy.json @@ -1,16 +1,45 @@ { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", + "metadata": { + "title": "CiscoUmbrella-GetDomainInfo", + "description": "This playbook showcases an example of triggering an incident within a targeted Teams channel and opening up a ticket within Service Now. Additionally The playbook will also list playbooks that can be initiated from teams using an adaptive card and callbacks that will take action upon certain entities identified in the incident.", + "prerequisites": [ + "1. ServiceNow Instance URL, Username, and password.", + "2. Access and authorization to enable API connectors", + "3. Teams Group ID and Alert Channel ID where the messages are to be posted in." + ], + "lastUpdateTime": "2021-06-29T10:00:00.000Z", + "entities": [ + "Account", + "Url", + "Host" + ], + "tags": [ + "Sync", + "Notification", + "Teams Response" + ], + "support": { + "tier": "community" + }, + "author": { + "name": "Jing Nghik" + } + }, "parameters": { "PlaybookName": { "defaultValue": "CiscoUmbrella-GetDomainInfo", "type": "String" + }, + "customApis_ciscoumbrellainvestigate_name": { + "defaultValue": "CiscoUmbrellaInvestigateAPI", + "type": "String" } }, "variables": { "AzureSentinelConnectionName": "[concat('azuresentinel-', parameters('PlaybookName'))]", - "CiscoUmbrellaInvestigateAPIConnectionName": "[concat('ciscoumbrellainvestigate-connection-', parameters('PlaybookName'))]", - "customApis_ciscoumbrellainvestigate_name": "CiscoUmbrellaInvestigateAPI" + "CiscoUmbrellaInvestigateAPIConnectionName": "[concat('ciscoumbrellainvestigate-connection-', parameters('PlaybookName'))]" }, "resources": [ { @@ -36,7 +65,7 @@ "displayName": "[variables('CiscoUmbrellaInvestigateAPIConnectionName')]", "customParameterValues": {}, "api": { - "id": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', resourceGroup().name, '/providers/Microsoft.Web/customApis/', variables('customApis_ciscoumbrellainvestigate_name'))]" + "id": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', resourceGroup().name, '/providers/Microsoft.Web/customApis/', parameters('customApis_ciscoumbrellainvestigate_name'))]" } } }, @@ -239,7 +268,7 @@ "ciscoumbrellainvestigate": { "connectionId": "[resourceId('Microsoft.Web/connections', variables('CiscoUmbrellaInvestigateAPIConnectionName'))]", "connectionName": "[variables('CiscoUmbrellaInvestigateAPIConnectionName')]", - "id": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', resourceGroup().name, '/providers/Microsoft.Web/customApis/', variables('customApis_ciscoumbrellainvestigate_name'))]" + "id": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', resourceGroup().name, '/providers/Microsoft.Web/customApis/', parameters('customApis_ciscoumbrellainvestigate_name'))]" } } } diff --git a/Solutions/CiscoUmbrella/SolutionMetadata.json b/Solutions/CiscoUmbrella/SolutionMetadata.json new file mode 100644 index 0000000000..162866ecb7 --- /dev/null +++ b/Solutions/CiscoUmbrella/SolutionMetadata.json @@ -0,0 +1,16 @@ +{ + "publisherId": "azuresentinel", + "offerId": "azure-sentinel-solution-ciscoumbrella", + "firstPublishDate": "2022-04-01", + "providers": [ "Microsoft" ], + "categories": { + "domains": [ "Security - Cloud Security" ], + "verticals": [] + }, + "support": { + "tier": "Microsoft", + "name": "Microsoft Corporation", + "email": "support@microsoft.com", + "link": "https://support.microsoft.com/" + } +} \ No newline at end of file diff --git a/Solutions/Okta Single Sign-On/Playbooks/OktaCustomConnector/azuredeploy.json b/Solutions/Okta Single Sign-On/Playbooks/OktaCustomConnector/azuredeploy.json index 4e08164bf9..f781552cc7 100644 --- a/Solutions/Okta Single Sign-On/Playbooks/OktaCustomConnector/azuredeploy.json +++ b/Solutions/Okta Single Sign-On/Playbooks/OktaCustomConnector/azuredeploy.json @@ -21,7 +21,6 @@ } } }, - "variables": {}, "resources": [ { @@ -60,8 +59,6 @@ "version": "1.0" }, "host": "$substring([parameters('Service EndPoint')],8 )", - - "basePath": "/", "schemes": [ "https" ], "consumes": [], @@ -505,7 +502,6 @@ ] } }, - "/api/v1/users/{userId}/lifecycle/expire_password": {}, "/api/v1/users/{userId}/lifecycle/reset_password": { "post": { "responses": { @@ -628,7 +624,6 @@ ] } }, - "": {}, "/api/v1/groups/{groupId}/users/{userId}": { "delete": { "responses": { diff --git a/Tools/Create-Azure-Sentinel-Solution/README.md b/Tools/Create-Azure-Sentinel-Solution/README.md index 6c71de1f53..0818d8e41d 100644 --- a/Tools/Create-Azure-Sentinel-Solution/README.md +++ b/Tools/Create-Azure-Sentinel-Solution/README.md @@ -32,6 +32,8 @@ The packaging tool detailed below provides an easy way to generate your solution Clone the repository [Azure-Sentinel](https://github.com/Azure/Azure-Sentinel) to `C:\One`. +For creating solution packages with Template Spec Resource, please refer the instructions mentioned in [Readme](https://github.com/Azure/Azure-Sentinel/blob/master/Tools/Create-Azure-Sentinel-Solution/V2/README.md) File. + ### Create Input File Create an input file and place it in the path `C:\One\Azure-Sentinel\Tools\Create-Sentinel-Solution\input`. @@ -46,8 +48,11 @@ Create an input file and place it in the path `C:\One\Azure-Sentinel\Tools\Creat * Name: Solution Name - Ex. "Symantec Endpoint Protection" * Author: Author Name+Email of Solution - Ex. "Eli Forbes - v-eliforbes@microsoft.com" * Logo: Link to the Logo used in createUiDefinition.json + * - NOTE: This field is only recommended for Azure Global Cloud. It is not recommended for solutions in Azure Government Cloud as the image will not be shown properly. * Description: Solution Description used in createUiDefinition.json. Can include markdown. - * WorkbookDescription: Workbook description(s), generally from Workbooks Metadata. This field can be a string if 1 description is used, and an array if multiple are used. + * WorkbookDescription: Workbook description(s), generally from Workbooks' Metadata. This field can be a string if 1 description is used across all, and an array if multiple are used. + * PlaybookDescription: Playbook description(s), generally from Playbooks' Metadata. This field can be a string if 1 description is used across all, and an array if multiple are used. + * WatchlistDescription: Watchlist description(s), generally from Watchlists' Property data. This field can be a string if 1 description is used across all, and an array if multiple are used. This field is used if the description from the Watchlist resource is not desired in the Create-UI. * Workbooks, Analytic Rules, Playbooks, etc.: These fields take arrays of paths relative to the repo root, or BasePath if provided. * SavedSearches: This input assumes a format of any of the following: * -- Direct export via API (see https://docs.microsoft.com/rest/api/loganalytics/saved-searches/list-by-workspace) @@ -56,8 +61,9 @@ Create an input file and place it in the path `C:\One\Azure-Sentinel\Tools\Creat * * - NOTE: Playbooks field can take standard Playbooks, Custom Connectors, and Function Apps * BasePath: Optional base path to use. Either Internet URL or File Path. Default is repo root (https://raw.githubusercontent.com/Azure/Azure-Sentinel/master/) - * Version: Version to be used during package creation + * Version: Version to be used during package creation. We should use any version >= 2.0.0 in case solution needs to be packaged for Template Spec * Metadata: Name of metadata file for the Solution, path is to be considered from BasePath. + * TemplateSpec: Boolean value used to determine whether the package should be generated as a template spec */ { "Name": "{SolutionName}", @@ -68,14 +74,17 @@ Create an input file and place it in the path `C:\One\Azure-Sentinel\Tools\Creat "Workbooks": [], "Analytic Rules": [], "Playbooks": [], + "PlaybookDescription": ["{Description of playbook}"], "Parsers": [], "SavedSearches": [], "Hunting Queries": [], "Data Connectors": [], "Watchlists": [], + "WatchlistDescription": [], "BasePath": "{Path to Solution Content}", "Version": "1.0.0", "Metadata": "{Name of Solution Metadata file}", + "TemplateSpec": false } ``` @@ -107,7 +116,8 @@ Create an input file and place it in the path `C:\One\Azure-Sentinel\Tools\Creat ], "BasePath": "https://raw.githubusercontent.com/Azure/Azure-Sentinel/master/Solutions/McAfeeePO/", "Version": "1.0.0", - "Metadata": "SolutionMetadata.json" + "Metadata": "SolutionMetadata.json", + "TemplateSpec": false } ``` @@ -131,7 +141,7 @@ Create a file and place it in the base path of solution `https://raw.githubuser * providers: Provider of the solution. Specify one or many providers as a comma separated list as applicable for the solution - Ex. Cisco, Checkpoint, Microsoft * categories: Domain and Vertical applicability of the solution. There can be multiple domain and/or vertical categories applicable to the same solution which can be represented as an array. For e.g. Domains - "Security - Network", "Application", etc. and Vertical - "Healthcare", "Finance". Refer to the [Microsoft Sentinel content and solutions categories documentation](https://aka.ms/sentinelcontentcategories) for a complete list of valid Microsoft Sentinel categories. * support: Name, Email, Tier and Link for the solution support details. - * - NOTE: Additional metadata properties like Version, Author, etc. are used by the packaging tool based on the values provided in the input file. Format specified in the example below. Refer to [Microsoft + * - NOTE: Additional metadata properties like Version, Author, etc. are used by the packaging tool based on the values provided in the input file. Format specified in the example below. Refer to [Microsoft content and support documentation](https://aka.ms/sentinelcontentsupportmodel) for further information. */ { diff --git a/Tools/Create-Azure-Sentinel-Solution/V2/README.md b/Tools/Create-Azure-Sentinel-Solution/V2/README.md new file mode 100644 index 0000000000..7f28f6c468 --- /dev/null +++ b/Tools/Create-Azure-Sentinel-Solution/V2/README.md @@ -0,0 +1,363 @@ +# Microsoft Sentinel Solutions Packaging Tool Guidance + +Microsoft Sentinel Solutions provide an in-product experience for central discoverability, single-step deployment, and enablement of end-to-end product and/or domain and/or vertical scenarios in Microsoft Sentinel. This experience is powered by Azure Marketplace for Solutions' discoverability, deployment and enablement and Microsoft Partner Center for Solutions’ authoring and publishing. Refer to details in [Microsoft Sentinel solutions documentation](https://aka.ms/azuresentinelsolutionsdoc). Detailed partner guidance for authoring and publishing solutions is covered in [building Microsoft Sentinel solutions guidance](https://aka.ms/sentinelsolutionsbuildguide). + +The packaging tool detailed below provides an easy way to generate your solution package of choice in an automated manner and enables validation of the package generated as well. You can package different types of Microsoft Sentinel content that includes a combination of data connectors, parsers or Kusto Functions, workbooks, analytic rules, hunting queries, Azure Logic apps custom connectors, playbooks and watchlists. + +## Setup + +- Install PowerShell 7.1+ + + - If you already have PowerShell 5.1, please follow this [upgrade guide](https://docs.microsoft.com/powershell/scripting/install/migrating-from-windows-powershell-51-to-powershell-7?view=powershell-7.1). + + - If you do not already have PowerShell, please follow this [installation guide](https://docs.microsoft.com/powershell/scripting/install/installing-powershell-core-on-windows?view=powershell-7.1). + +- Install Node.js + + - The installation process can be started from [their website](https://nodejs.org/). + +- Install YAML Toolkit for Powershell + + - `Install-Module powershell-yaml` + +- *For ease of editing, it's recommended to use VSCode with the 'Azure Resource Manager (ARM) Tools' extension installed* + + - Install [VSCode](https://code.visualstudio.com/). + + - Install the [Azure Resource Manager (ARM) Tools Extension](https://marketplace.visualstudio.com/items?itemName=msazurermtools.azurerm-vscode-tools). + + - This extension provides language support, resource auto-completion, and automatic template validation within your IDE. + +## Creating Solution Package + +Clone the repository [Azure-Sentinel](https://github.com/Azure/Azure-Sentinel) to `C:\One`. + +### Create Input File + +Create an input file and place it in the path `C:\One\Azure-Sentinel\Tools\Create-Sentinel-Solution\input`. + +#### **Input File Format:** + +```json +/** + * Solution Automation Input File Json + * ----------------------------------------------------- + * The purpose of this json is to provide detail on the various fields the input file can have. + * Name: Solution Name - Ex. "Symantec Endpoint Protection" + * Author: Author Name+Email of Solution - Ex. "Eli Forbes - v-eliforbes@microsoft.com" + * Logo: Link to the Logo used in createUiDefinition.json + * - NOTE: This field is only recommended for Azure Global Cloud. It is not recommended for solutions in Azure Government Cloud as the image will not be shown properly. + * Description: Solution Description used in createUiDefinition.json. Can include markdown. + * WorkbookDescription: Workbook description(s), generally from Workbooks' Metadata. This field can be a string if 1 description is used across all, and an array if multiple are used. + * PlaybookDescription: Playbook description(s), generally from Playbooks' Metadata. This field can be a string if 1 description is used across all, and an array if multiple are used. + * WatchlistDescription: Watchlist description(s), generally from Watchlists' Property data. This field can be a string if 1 description is used across all, and an array if multiple are used. This field is used if the description from the Watchlist resource is not desired in the Create-UI. + * Workbooks, Analytic Rules, Playbooks, etc.: These fields take arrays of paths relative to the repo root, or BasePath if provided. + * SavedSearches: This input assumes a format of any of the following: + * -- Direct export via API (see https://docs.microsoft.com/rest/api/loganalytics/saved-searches/list-by-workspace) + * -- Array of SavedSearch resources + * -- Raw ARM template + * + * - NOTE: Playbooks field can take standard Playbooks, Custom Connectors, and Function Apps + * BasePath: Optional base path to use. Either Internet URL or File Path. Default is repo root (https://raw.githubusercontent.com/Azure/Azure-Sentinel/master/) + * Version: Version to be used during package creation. We should use any version >= 2.0.0 in case solution needs to be packaged for Template Spec + * Metadata: Name of metadata file for the Solution, path is to be considered from BasePath. + * TemplateSpec: Boolean value used to determine whether the package should be generated as a template spec + */ +{ + "Name": "{SolutionName}", + "Author": "{AuthorName - Email}", + "Logo": "", + "Description": "{Solution Description}", + "WorkbookDescription": ["{Description of workbook}"], + "Workbooks": [], + "WorkbookBladeDescription: string; //Description used in the CreateUiDefinition.json for Workbooks Blade + "AnalyticalRuleBladeDescription": "{//Description used in the CreateUiDefinition.json for Analytical Rule Blade" + "HuntingQueryBladeDescription": "//Description used in the CreateUiDefinition.json for Hunting Query Blade" + "PlaybooksBladeDescription": "//Description used in the CreateUiDefinition.json for Playbook Blade" + "Analytic Rules": [], + "Playbooks": [], + "PlaybookDescription": ["{Description of playbook}"], + "Parsers": [], + "SavedSearches": [], + "Hunting Queries": [], + "Data Connectors": [], + "Watchlists": [], + "WatchlistDescription": [], + "BasePath": "{Path to Solution Content}", + "Version": "2.0.0", + "Metadata": "{Name of Solution Metadata file}", + "TemplateSpec": true, + "Is1PConnector": false +} + +``` + +#### **Example of Input File: Solution_McAfeePO.json** + +```json +{ + "Name": "Cisco Umbrella", + "Author": "Microsoft - support@microsoft.com", + "Logo": "", + "Description": "The [Cisco Umbrella](https://umbrella.cisco.com/) solution for Microsoft Sentinel enables you to ingest [Cisco Umbrella events](https://docs.umbrella.com/deployment-umbrella/docs/log-formats-and-versioning) stored in Amazon S3 into Microsoft Sentinel using the Amazon S3 REST API. + + **Underlying Microsoft Technologies used:**\n\nThis solution takes a dependency on the following technologies, and some of these dependencies either may be in [Preview](https://azure.microsoft.com/support/legal/preview-supplemental-terms/) state or might result in additional ingestion or operational costs: + a. [Azure Monitor HTTP Data Collector API](https://docs.microsoft.com/azure/azure-monitor/logs/data-collector-api) + b. [Azure Functions](https://azure.microsoft.com/services/functions/#overview) ", + "WorkbookBladeDescription": "This Microsoft Sentinel Solution installs workbooks. Workbooks provide a flexible canvas for data monitoring, analysis, and the creation of rich visual reports within the Azure portal. They allow you to tap into one or many data sources from Microsoft Sentinel and combine them into unified interactive experiences.", + "AnalyticalRuleBladeDescription": "This solution installs the following analytic rule templates. After installing the solution, create and enable analytic rules in Manage solution view. ", + "HuntingQueryBladeDescription": "This solution installs the following hunting queries. After installing the solution, run these hunting queries to hunt for threats in Manage solution view", + "PlaybooksBladeDescription": "This solution installs the following Playbook templates. After installing the solution, playbooks can be managed in the Manage solution view. ", + "Data Connectors": [ + "DataConnectors/CiscoUmbrella/CiscoUmbrella_API_FunctionApp.json" + ], + "Parsers": [ + "Solutions/CiscoUmbrella/Parsers/Cisco_Umbrella" + ], + "Hunting Queries": [ + "Solutions/CiscoUmbrella/Hunting Queries/CiscoUmbrellaAnomalousFQDNsforDomain.yaml", + "Solutions/CiscoUmbrella/Hunting Queries/CiscoUmbrellaBlockedUserAgents.yaml", + "Solutions/CiscoUmbrella/Hunting Queries/CiscoUmbrellaDNSErrors.yaml", + "Solutions/CiscoUmbrella/Hunting Queries/CiscoUmbrellaDNSRequestsUunreliableCategory.yaml", + "Solutions/CiscoUmbrella/Hunting Queries/CiscoUmbrellaHighCountsOfTheSameBytesInSize.yaml", + "Solutions/CiscoUmbrella/Hunting Queries/CiscoUmbrellaHighValuesOfUploadedData.yaml", + "Solutions/CiscoUmbrella/Hunting Queries/CiscoUmbrellaPossibleConnectionC2.yaml", + "Solutions/CiscoUmbrella/Hunting Queries/CiscoUmbrellaPossibleDataExfiltration.yaml", + "Solutions/CiscoUmbrella/Hunting Queries/CiscoUmbrellaProxyAllowedUnreliableCategory.yaml", + "Solutions/CiscoUmbrella/Hunting Queries/CiscoUmbrellaRequestsUncategorizedURI.yaml" + ], + "Analytic Rules": [ + "Solutions/CiscoUmbrella/Analytic Rules/CiscoUmbrellaConnectionNon-CorporatePrivateNetwork.yaml", + "Solutions/CiscoUmbrella/Analytic Rules/CiscoUmbrellaConnectionToUnpopularWebsiteDetected.yaml", + "Solutions/CiscoUmbrella/Analytic Rules/CiscoUmbrellaCryptoMinerUserAgentDetected.yaml", + "Solutions/CiscoUmbrella/Analytic Rules/CiscoUmbrellaEmptyUserAgentDetected.yaml", + "Solutions/CiscoUmbrella/Analytic Rules/CiscoUmbrellaHackToolUserAgentDetected.yaml", + "Solutions/CiscoUmbrella/Analytic Rules/CiscoUmbrellaPowershellUserAgentDetected.yaml", + "Solutions/CiscoUmbrella/Analytic Rules/CiscoUmbrellaRareUserAgentDetected.yaml", + "Solutions/CiscoUmbrella/Analytic Rules/CiscoUmbrellaRequestAllowedHarmfulMaliciousURICategory.yaml", + "Solutions/CiscoUmbrella/Analytic Rules/CiscoUmbrellaRequestBlocklistedFileType.yaml", + "Solutions/CiscoUmbrella/Analytic Rules/CiscoUmbrellaURIContainsIPAddress.yaml" + ], + "Workbooks": [ + "Solutions/CiscoUmbrella/Workbooks/CiscoUmbrella.json" + ], + "BasePath": "C:\\GitHub\\Azure-Sentinel", + "Version": "2.0.0", + "Metadata": "SolutionMetadata.json", + "TemplateSpec": true, + "Is1PConnector": false +} +``` + +### Create Solution Metadata File + +Create a file and place it in the base path of solution `https://raw.githubusercontent.com/Azure/Azure-Sentinel/master/Solutions/McAfeeePO/`. +* Refer to the [Microsoft Sentinel content and solutions categories documentation](https://aka.ms/sentinelcontentcategories) for a complete list of valid Microsoft Sentinel categories. +* Refer to [Microsoft Sentinel content and support documentation](https://aka.ms/sentinelcontentsupportmodel) for information on valid support models. + +#### **Metadata File Format:** + +```json +/** + * Solution Automation Metadata File Json + * ----------------------------------------------------- + * The purpose of this json is to provide detail on the various fields the metadata solution can have. Refer to the metadata schema and example provided after the definitions for further context. + * publisherId: An identifier that's used by Partner Center to uniquely identify the publisher associated with a commercial marketplace account.- Ex. "azuresentinel", "CheckPoint", "semperis" + * offerId: Id of the Offer of Solution - Ex. "azure-sentinel-solution-ciscoaci", "azure-sentinel-solution-semperis-dsp" + * firstPublishDate: Solution first published date + * lastPublishDate: Latest published date of Solution + * providers: Provider of the solution. Specify one or many providers as a comma separated list as applicable for the solution - Ex. Cisco, Checkpoint, Microsoft + * categories: Domain and Vertical applicability of the solution. There can be multiple domain and/or vertical categories applicable to the same solution which can be represented as an array. For e.g. Domains - "Security - Network", "Application", etc. and Vertical - "Healthcare", "Finance". Refer to the [Microsoft Sentinel content and solutions categories documentation](https://aka.ms/sentinelcontentcategories) for a complete list of valid Microsoft Sentinel categories. + * support: Name, Email, Tier and Link for the solution support details. + * - NOTE: Additional metadata properties like Version, Author, etc. are used by the packaging tool based on the values provided in the input file. Format specified in the example below. Refer to [Microsoft + content and support documentation](https://aka.ms/sentinelcontentsupportmodel) for further information. + */ +{ + "publisherId": {Id of Publisher}, + "offerId": {Solution Offer Id}, + "firstPublishDate": {Solution First Published Date}, + "lastPublishDate": {Solution recent Published Date}, + "providers": {Solution provider list}, + "categories": { + "domains" : {Solution category domain list}, + "verticals": {Solution category vertical list}, + }, + "support": { + "name": {Publisher ID}, + "email": {Email for Solution Support}, + "tier": {Support Tier}, + "link": {Link of Support contacts for Solution}, + } +} + +``` + +#### **Example of Input File: SolutionMetadata.json** + +```json +{ + "publisherId": "azuresentinel", + "offerId": "azure-sentinel-solution-mcafeeepo", + "firstPublishDate": "2021-03-26", + "lastPublishDate": "2021-08-09", + "providers": ["Cisco"], + "categories": { + "domains" : ["Security - Network"], + "verticals": [] + }, + "support": { + "name": "Microsoft Corporation", + "email": "support@microsoft.com", + "tier": "Microsoft", + "link": "https://support.microsoft.com" + } +}  +``` + +### Generate Solution Package + +To generate the solution package from the given input file, run the `createSolutionV2.ps1` script in the automation folder, `Tools/Create-Azure-Sentinel-Solution/V2`. +> Ex. From repository root, run: `./Tools/Create-Azure-Sentinel-Solution/V2/createSolutionV2.ps1` + +This will generate and compress the solution package, and name the package using the version provided in the input file. + +The package consists of the following files: + +* `createUIDefinition.json`: Template containing the definition for the Deployment Creation UI + +* `mainTemplate.json`: Template containing Deployable Resources + +These files will be created in the solution's `Package` folder with respect to the resources provided in the given input file. For every new modification to the files after the initial version of package, a new zip file should be created with an updated version name (1.0.1, 1.0.2, etc.) containing modified `createUIDefinition.json` and `mainTemplate.json` files. + +Upon package creation, the automation will automatically import and run validation on the generated files using the Azure Toolkit / TTK CLI tool. + +### Azure Toolkit Validation + +The Azure Toolkit Validation is run automatically after package generation. However, if you make any manual edits to the template after the package is generated, you'll need to manually run the Azure Toolkit technical validation on your solution to check the end result. + +If you've already run the package creation tool in your current PowerShell instance, you should have the validation command imported and available, otherwise follow the steps below to install. + +#### Azure Toolkit Validation Setup + +- Clone the [arm-ttk repository](https://github.com/Azure/arm-ttk) to `C:\One` + - If `C:\One` does not exist, create the folder. + - You may also choose a different folder, but properly reference it in the Profile script. +- Open your Powershell Profile script + - To find your Powershell Profile Script: + - Open Powershell. + - Type `$profile`, and hit enter. + - Your Powershell Profile script path will be output to the screen. + - Open the Profile script. +- Add the following line of code to your Profile script. + - `Import-Module C:\One\arm-ttk\arm-ttk\arm-ttk.psd1` +- Save and close your Profile script. +- Refresh your profile. + - Run the following command in Powershell: `& $profile` + - Alternatively, you can close and re-open your PowerShell window. + +#### Azure Toolkit Validation Usage + +- Navigate to the directory of your solution. +- Run: `Test-AzTemplate` + +### Manual Validation + +Once the package is created and Azure Toolkit technical validation is passing, one should manually validate that the package is created as desired. + +**1. Validate createUiDefinition.json:** + +* Open [CreateUISandbox](https://portal.azure.com/#blade/Microsoft_Azure_CreateUIDef/SandboxBlade). +* Copy json content from createUiDefinition.json (in the recent version). +* Clear that content in the editor and replace with copied content in step #2. +* Click on preview +* You should see the User Interface preview of data connector, workbook, etc., and descriptions you provided in input file. +* Check the description and User Interface of solution preview. + +**2. Validate maintemplate.json:** + +Validate `mainTemplate.json` by deploying the template in portal. +Follow these steps to deploy in portal: + +* Open up which launches the Azure portal with the needed private preview flags. +* Go to "Deploy a Custom Template" on the portal +* Select "Build your own template in Editor". +* Copy json content from `mainTemplate.json` (in the recent version). +* Clear that content in the editor and replace with copied content in step #3. +* Click Save and then progress to selecting subscription, Sentinel-enabled resource group, and corresponding workspace, etc., to complete the deployment. +* Click Review + Create to trigger deployment. +* Check if the deployment successfully completes. +* You should see the data connector, workbook, etc., deployed in the respective galleries and validate – let us know your feedback. + +### Known Failures + +#### VMSizes Must Match Template + +This will generally show as a warning but the test will be skipped. This will not be perceived as an error by the build. + +### Common Issues + +#### Template Should Not Contain Blanks + +This issue most commonly comes from the serialized workbook and playbooks, due to certain properties in the json having values of null, [], or {}. To fix this, remove these properties. + +#### IDs Should Be Derived from ResourceIDs + +Some IDs used, most commonly in resources of type `Microsoft.Web/connections`, tend to throw this error despite seeming to fit the expected format. To fix this define two variables, one which uses the problematic ID value, and another which references the first variable, then use this second variable as necessary in place of the ID value. See below for example of such a variable pair: + +```json +"variables": { + "playbook-1-connection-1": "[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Web/locations/', parameters('workspace-location'), '/managedApis/microsoftgraphsecurity')]", + "_playbook-1-connection-1": "[variables('playbook-1-connection-1')]" + } +``` + +#### ApiVersions Should Be Recent + +Some resources, particularly playbook-related resources, come in with outdated `apiVersion` properties, and depending on the version it may not be picked up as outdated by the validation. + +Please ensure that resources of the following types use the corresponding versions: + +```json +{ + "type": "Microsoft.Web/connections", + "apiVersion": "2018-07-01-preview", +} +``` + +```json +{ + "type": "Microsoft.Logic/workflows", + "apiVersion": "2019-05-01", +} +``` + +#### Parameters Must Be Referenced + +It's possible some default parameters may go unused, especially if the solution consists mainly of playbooks. On failure this check will output the unused parameter(s) that exist within the `mainTemplate.json` file. + +To fix this, remove the unused parameter from the `parameters` section of `mainTemplate.json`, and check the following common issue "Outputs Must Be Present In Template Parameters". + +#### Outputs Must Be Present In Template Parameters + +In most cases, this error is a result of removing an unused parameter reference from `mainTemplate.json`. To fix the error in such a case, remove the problematic output variable from the `outputs` section of `createUiDefinition.json`. + +Otherwise, the parameter will need be added in the `parameters` section of `mainTemplate.json` and referenced as necessary. + +#### Main Template Encoding Issues + +If you generate your solution package using a version of PowerShell under 7.1, you'll likely face encoding errors which cause issues within the `mainTemplate.json` file. + +The main encoding issue here will be that single-quote characters `'` are encoded into `\u0027`, and due to function references relying on single-quotes, this will break the template. + +To resolve this issue, it's recommended that you install PowerShell 7.1+ and re-generate the package. + +See [Setup](#setup) to install PowerShell 7.1+. + + +#### YAML Conversion Issues + +If the YAML Toolkit for PowerShell is not installed, you may experience errors related to converting `.yaml` files, for analytic rules or otherwise. + +To resolve this issue, it's recommended that you install the YAML Toolkit for Powershell. + +See [Setup](#setup) to install the YAML Toolkit for PowerShell. diff --git a/Tools/Create-Azure-Sentinel-Solution/V2/createSolutionV2.ps1 b/Tools/Create-Azure-Sentinel-Solution/V2/createSolutionV2.ps1 new file mode 100644 index 0000000000..4e2b8a1868 --- /dev/null +++ b/Tools/Create-Azure-Sentinel-Solution/V2/createSolutionV2.ps1 @@ -0,0 +1,2396 @@ +$jsonConversionDepth = 50 +$path = "$PSScriptRoot\input" + +function handleEmptyInstructionProperties ($inputObj) { + $outputObj = $inputObj | + Get-Member -MemberType *Property | + Select-Object -ExpandProperty Name | + Sort-Object | + ForEach-Object -Begin { $obj = New-Object PSObject } { + if (($null -eq $inputObj.$_) -or ($inputObj.$_ -eq "") -or ($inputObj.$_.Count -eq 0)) { + Write-Host "Removing empty property $_" + } + else { + $obj | Add-Member -memberType NoteProperty -Name $_ -Value $inputObj.$_ + } + } { $obj } + $outputObj +} +function removePropertiesRecursively ($resourceObj) { + foreach ($prop in $resourceObj.PsObject.Properties) { + $key = $prop.Name + $val = $prop.Value + if ($null -eq $val) { + $resourceObj.PsObject.Properties.Remove($key) + } + elseif ($val -is [System.Object[]]) { + if ($val.Count -eq 0) { + $resourceObj.PsObject.Properties.Remove($key) + } + else { + foreach ($item in $val) { + $itemIndex = $val.IndexOf($item) + $resourceObj.$key[$itemIndex] = $(removePropertiesRecursively $val[$itemIndex]) + } + } + } + else { + if ($val -is [PSCustomObject]) { + if ($($val.PsObject.Properties).Count -eq 0) { + $resourceObj.PsObject.Properties.Remove($key) + } + else { + $resourceObj.$key = $(removePropertiesRecursively $val) + if ($($resourceObj.$key.PsObject.Properties).Count -eq 0) { + $resourceObj.PsObject.Properties.Remove($key) + } + } + } + } + } + $resourceObj +} + +function queryResourceExists () { + foreach ($resource in $baseMainTemplate.resources) { + if ($resource.type -eq "Microsoft.OperationalInsights/workspaces") { + return $true + } + } + return $false +} + +function getQueryResourceLocation () { + for ($i = 0; $i -lt $baseMainTemplate.resources.Length; $i++) { + if ($baseMainTemplate.resources[$i].type -eq "Microsoft.OperationalInsights/workspaces") { + return $i + } + } +} + +foreach ($inputFile in $(Get-ChildItem $path)) { + $inputJsonPath = Join-Path -Path $path -ChildPath "$($inputFile.Name)" + + $contentToImport = Get-Content -Raw $inputJsonPath | Out-String | ConvertFrom-Json + $basePath = $(if ($contentToImport.BasePath) { $contentToImport.BasePath + "/" } else { "https://raw.githubusercontent.com/Azure/Azure-Sentinel/master/" }) + + # Content Counters - (for adding numbering to each item) + $analyticRuleCounter = 1 + $connectorCounter = 1 + $workbookCounter = 1 + $playbookCounter = 1 + $parserCounter = 1 + $savedSearchCounter = 1 + $huntingQueryCounter = 1 + $watchlistCounter = 1 + + # Convenience Variables + $solutionName = $contentToImport.Name + + + # Base JSON Object Paths + $baseMainTemplatePath = "$PSScriptRoot/templating/baseMainTemplate.json" + $baseCreateUiDefinitionPath = "$PSScriptRoot/templating/baseCreateUiDefinition.json" + $metadataPath = "$PSScriptRoot/../../../Solutions/$($contentToImport.Name)/$($contentToImport.Metadata)" + + $workbookMetadataPath = "https://raw.githubusercontent.com/Azure/Azure-Sentinel/master/" + # Base JSON Objects + $baseMainTemplate = Get-Content -Raw $baseMainTemplatePath | Out-String | ConvertFrom-Json + $baseCreateUiDefinition = Get-Content -Raw $baseCreateUiDefinitionPath | Out-String | ConvertFrom-Json + $baseMetadata = Get-Content -Raw $metadataPath | Out-String | ConvertFrom-Json + + $DependencyCriteria = @(); + $customConnectorsList = @{}; + $metadataAuthor = $contentToImport.Author.Split(" - "); + $solutionId = $baseMetadata.publisherId + "." + $baseMetadata.offerId + $baseMainTemplate.variables | Add-Member -NotePropertyName "solutionId" -NotePropertyValue $solutionId + $baseMainTemplate.variables | Add-Member -NotePropertyName "_solutionId" -NotePropertyValue "[variables('solutionId')]" + if($null -ne $metadataAuthor[1]) + { + $baseMainTemplate.variables | Add-Member -NotePropertyName "email" -NotePropertyValue $($metadataAuthor[1]) + $baseMainTemplate.variables | Add-Member -NotePropertyName "_email" -NotePropertyValue "[variables('email')]" + } + + foreach ($objectProperties in $contentToImport.PsObject.Properties) { + # Access the value of the property + if ($objectProperties.Value -is [System.Array]) { + foreach ($file in $objectProperties.Value) { + $finalPath = $basePath + $file + $rawData = $null + try { + Write-Host "Downloading $finalPath" + $rawData = (New-Object System.Net.WebClient).DownloadString($finalPath) + } + catch { + Write-Host "Failed to download $finalPath -- Please ensure that it exists in $([System.Uri]::EscapeUriString($basePath))" -ForegroundColor Red + break; + } + + try { + $json = ConvertFrom-Json $rawData -ErrorAction Stop; # Determine whether content is JSON or YAML + $validJson = $true; + } + catch { + $validJson = $false; + } + #Replace the special characters in the solution name. + function Replace-SpecialChars { + param($InputString,$Type) + if ($Type.ToLower() -eq 'solutionname') { + $SpecialChars = '[#?\{\[\(\)\]\}]' + $Replacement = ' ' + } + elseif ($Type.ToLower() -eq 'filename') { + $SpecialChars = '[#?\{\[\(\)\]\}]' + $Replacement = '' + } + else { + $SpecialChars = '[#?\{\[\(\)\]\}]' + $Replacement = '' + } + return $InputString -replace $SpecialChars,$Replacement + } + + if ($validJson) { + # If valid JSON, must be Workbook or Playbook + $objectKeyLowercase = $objectProperties.Name.ToLower() + if ($objectKeyLowercase -eq "workbooks") { + Write-Host "Generating Workbook using $file" + $solutionRename = Replace-SpecialChars -InputString $solutionName -Type 'solutionname' + $fileName = Split-Path $file -leafbase; + $fileName = Replace-SpecialChars -InputString $fileName -Type 'filename' + $workbookKey = $fileName; + $fileName = $fileName + "Workbook"; + + if ($workbookCounter -eq 1) { + # Add workbook source variables + if (!$contentToImport.TemplateSpec){ + $baseMainTemplate.variables | Add-Member -NotePropertyName "workbook-source" -NotePropertyValue "[concat(resourceGroup().id, '/providers/Microsoft.OperationalInsights/workspaces/',parameters('workspace'))]" + $baseMainTemplate.variables | Add-Member -NotePropertyName "_workbook-source" -NotePropertyValue "[variables('workbook-source')]" + }; + $baseWorkbookStep = [PSCustomObject] @{ + name = "workbooks"; + label = "Workbooks"; + subLabel = [PSCustomObject] @{ + preValidation = "Configure the workbooks"; + postValidation = "Done"; + }; + bladeTitle = "Workbooks"; + elements = @( + [PSCustomObject] @{ + name = "workbooks-text"; + type = "Microsoft.Common.TextBlock"; + options = [PSCustomObject] @{ + text = $contentToImport.WorkbookBladeDescription ? $contentToImport.WorkbookBladeDescription : "This Microsoft Sentinel Solution installs workbooks. Workbooks provide a flexible canvas for data monitoring, analysis, and the creation of rich visual reports within the Azure portal. They allow you to tap into one or many data sources from Microsoft Sentinel and combine them into unified interactive experiences."; + } + }, + [PSCustomObject] @{ + name = "workbooks-link"; + type = "Microsoft.Common.TextBlock"; + options = [PSCustomObject] @{ + link= [PSCustomObject] @{ + label= "Learn more"; + uri= "https://docs.microsoft.com/azure/sentinel/tutorial-monitor-your-data" + } + } + } + ) + } + $baseCreateUiDefinition.parameters.steps += $baseWorkbookStep + + if(!$contentToImport.TemplateSpec) + { + #Add formattedTimeNow parameter since workbooks exist + $timeNowParameter = [PSCustomObject]@{ + type = "string"; + defaultValue = "[utcNow('g')]"; + metadata = [PSCustomObject]@{ + description = "Appended to workbook displayNames to make them unique"; + } + } + $baseMainTemplate.parameters | Add-Member -MemberType NoteProperty -Name "formattedTimeNow" -Value $timeNowParameter + } + } + try { + $data = $rawData + # Serialize workbook data + $serializedData = $data | ConvertFrom-Json -Depth $jsonConversionDepth + # Remove empty braces + $serializedData = $(removePropertiesRecursively $serializedData) | ConvertTo-Json -Compress -Depth $jsonConversionDepth | Out-String + } + catch { + Write-Host "Failed to serialize $file" -ForegroundColor Red + break; + } + $workbookDescriptionText = $(if ($contentToImport.WorkbookDescription -and $contentToImport.WorkbookDescription -is [System.Array]) { $contentToImport.WorkbookDescription[$workbookCounter - 1] } elseif ($contentToImport.WorkbookDescription -and $contentToImport.WorkbookDescription -is [System.String]) { $contentToImport.WorkbookDescription } else { "" }) + #creating parameters in mainTemplate + $workbookIDParameterName = "workbook$workbookCounter-id" + $workbookNameParameterName = "workbook$workbookCounter-name" + $workbookIDParameter = [PSCustomObject] @{ type = "string"; defaultValue = "[newGuid()]"; minLength = 1; metadata = [PSCustomObject] @{ description = "Unique id for the workbook" }; } + + + if(!$contentToImport.TemplateSpec) + { + $baseMainTemplate.parameters | Add-Member -MemberType NoteProperty -Name $workbookIDParameterName -Value $workbookIDParameter + } + + # Create Workbook Resource Object + $newWorkbook = [PSCustomObject]@{ + type = "Microsoft.Insights/workbooks"; + name = "[parameters('workbook$workbookCounter-id')]"; + location = "[parameters('workspace-location')]"; + kind = "shared"; + apiVersion = "2021-08-01"; + metadata = [PSCustomObject]@{}; + properties = [PSCustomObject] @{ + displayName = $contentToImport.Workbooks ? "[parameters('workbook$workbookCounter-name')]" : "[concat(parameters('workbook$workbookCounter-name'), ' - ', parameters('formattedTimeNow'))]"; + serializedData = $serializedData; + version = "1.0"; + sourceId = $contentToImport.TemplateSpec? "[variables('workspaceResourceId')]" : "[variables('_workbook-source')]"; + category = "sentinel" + } + } + + if($contentToImport.TemplateSpec) { + #Getting Workbook Metadata dependencies from Github + $workbookData = $null + $workbookFinalPath = $workbookMetadataPath + 'Tools/Create-Azure-Sentinel-Solution/V2/WorkbookMetadata/WorkbooksMetadata.json'; + try { + Write-Host "Downloading $workbookFinalPath" + $workbookData = (New-Object System.Net.WebClient).DownloadString($workbookFinalPath) + $dependencies = $workbookData | ConvertFrom-Json | Where-Object {($_.templateRelativePath.split('.')[0].ToLower() -eq $workbookKey.ToLower())} + $WorkbookDependencyCriteria = @(); + foreach($dataTypesDependencies in $dependencies.dataTypesDependencies) + { + $dataTypeObject = New-Object PSObject + $dataTypeObject | Add-Member -MemberType NoteProperty -Name "contentId" -Value "$dataTypesDependencies" + $dataTypeObject | Add-Member -MemberType NoteProperty -Name "kind" -Value "DataType" + $WorkbookDependencyCriteria += $dataTypeObject + } + foreach($dataConnectorsDependencies in $dependencies.dataConnectorsDependencies) + { + $dataConnectorObject = New-Object PSObject + $dataConnectorObject | Add-Member -MemberType NoteProperty -Name "contentId" -Value "$dataConnectorsDependencies" + $dataConnectorObject | Add-Member -MemberType NoteProperty -Name "kind" -Value "DataConnector" + $WorkbookDependencyCriteria += $dataConnectorObject + } + $workbookDependencies = [PSCustomObject]@{ + operator = "AND"; + criteria = $WorkbookDependencyCriteria; + }; + $newWorkbook.metadata | Add-Member -MemberType NoteProperty -Name "description" -Value "$($dependencies.description)" + } + catch { + Write-Host "TemplateSpec Workbook Metadata Dependencies errors occurred: $($_.Exception.Message)" -ForegroundColor Red + break; + } + + $workbookNameParameter = [PSCustomObject] @{ type = "string"; defaultValue = $dependencies.title; minLength = 1; metadata = [PSCustomObject] @{ description = "Name for the workbook" }; } + $baseMainTemplate.variables | Add-Member -NotePropertyName "workbookVersion$workbookCounter" -NotePropertyValue "$($dependencies.version)" + $baseMainTemplate.variables | Add-Member -NotePropertyName "workbookContentId$workbookCounter" -NotePropertyValue "$($dependencies.workbookKey)" + $baseMainTemplate.parameters | Add-Member -MemberType NoteProperty -Name $workbookNameParameterName -Value $workbookNameParameter + $baseMainTemplate.variables | Add-Member -NotePropertyName "workbookId$workbookCounter" -NotePropertyValue "[resourceId('Microsoft.Insights/workbooks', variables('workbookContentId$workbookCounter'))]" + $baseMainTemplate.variables | Add-Member -NotePropertyName "workbookTemplateSpecName$workbookCounter" -NotePropertyValue "[concat(parameters('workspace'),'-wb-',uniquestring(variables('_workbookContentId$workbookCounter')))]" + $baseMainTemplate.variables | Add-Member -NotePropertyName "_workbookContentId$workbookCounter" -NotePropertyValue "[variables('workbookContentId$workbookCounter')]" + $DependencyCriteria += [PSCustomObject]@{ + kind = "Workbook"; + contentId = "[variables('_workbookContentId$workbookCounter')]"; + version = "[variables('workbookVersion$workbookCounter')]"; + }; + + # Add workspace resource ID if not available + if (!$baseMainTemplate.variables.workspaceResourceId) { + $baseMainTemplate.variables | Add-Member -NotePropertyName "workspaceResourceId" -NotePropertyValue "[resourceId('microsoft.OperationalInsights/Workspaces', parameters('workspace'))]" + } + + # Add base templateSpec + $baseWorkbookTemplateSpec = [PSCustomObject]@{ + type = "Microsoft.Resources/templateSpecs"; + apiVersion = "2021-05-01"; + name = "[variables('workbookTemplateSpecName$workbookCounter')]"; + location = "[parameters('workspace-location')]"; + tags = [PSCustomObject]@{ + "hidden-sentinelWorkspaceId" = "[variables('workspaceResourceId')]"; + "hidden-sentinelContentType" = "Workbook"; + }; + properties = [PSCustomObject]@{ + description = "$($solutionName) Workbook with template"; + displayName = "$($solutionName) workbook template"; + } + } + $newWorkbook.name = "[variables('workbookContentId$workbookCounter')]" + $baseMainTemplate.resources += $baseWorkbookTemplateSpec + $author = $contentToImport.Author.Split(" - "); + $authorDetails = [PSCustomObject]@{ + name = $author[0]; + }; + if($null -ne $author[1]) + { + $authorDetails | Add-Member -NotePropertyName "email" -NotePropertyValue "[variables('_email')]" + } + $workbookMetadata = [PSCustomObject]@{ + type = "Microsoft.OperationalInsights/workspaces/providers/metadata"; + apiVersion = "2022-01-01-preview"; + name = "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('Workbook-', last(split(variables('workbookId$workbookCounter'),'/'))))]"; + properties = [PSCustomObject]@{ + description = "$dependencies.description"; + parentId = "[variables('workbookId$workbookCounter')]" + contentId = "[variables('_workbookContentId$workbookCounter')]"; + kind = "Workbook"; + version = "[variables('workbookVersion$workbookCounter')]"; + source = [PSCustomObject]@{ + kind = "Solution"; + name = $contentToImport.Name; + sourceId = "[variables('_solutionId')]" + }; + author = $authorDetails; + support = $baseMetadata.support; + dependencies = $workbookDependencies; + } + } + + if($workbookDescriptionText -ne "") + { + $workbookMetadata | Add-Member -NotePropertyName "description" -NotePropertyValue $workbookDescriptionText + } + + # Add templateSpecs/versions resource to hold actual content + $workbookTemplateSpecContent = [PSCustomObject]@{ + type = "Microsoft.Resources/templateSpecs/versions"; + apiVersion = "2021-05-01"; + name = "[concat(variables('workbookTemplateSpecName$workbookCounter'),'/',variables('workbookVersion$workbookCounter'))]"; + location = "[parameters('workspace-location')]"; + tags = [PSCustomObject]@{ + "hidden-sentinelWorkspaceId" = "[variables('workspaceResourceId')]"; + "hidden-sentinelContentType" = "Workbook"; + }; + dependsOn = @( + "[resourceId('Microsoft.Resources/templateSpecs', variables('workbookTemplateSpecName$workbookCounter'))]" + ); + properties = [PSCustomObject]@{ + description = "$($fileName) Workbook with template version $($contentToImport.Version)"; + mainTemplate = [PSCustomObject]@{ + '$schema' = "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#"; + contentVersion = "[variables('workbookVersion$workbookCounter')]"; + parameters = [PSCustomObject]@{}; + variables = [PSCustomObject]@{}; + resources = @( + # workbook + $newWorkbook, + # Metadata + $workbookMetadata + ) + } + } + } + + $baseMainTemplate.resources += $workbookTemplateSpecContent + } + else { + $baseMainTemplate.resources += $newWorkbook + if ($contentToImport.Metadata) { + $baseMainTemplate.variables | Add-Member -NotePropertyName $fileName -NotePropertyValue $fileName + $baseMainTemplate.variables | Add-Member -NotePropertyName "_$fileName" -NotePropertyValue "[variables('$fileName')]" + $DependencyCriteria += [PSCustomObject]@{ + kind = "Workbook"; + contentId = "[variables('_$fileName')]"; + version = "[variables('workbookVersion$workbookCounter')]"; + }; + } + } + $workbookCounter += 1 + } + elseif ($objectKeyLowercase -eq "playbooks") { + Write-Host "Generating Playbook using $file" + $playbookData = $json + $playbookName = $(if ($playbookData.parameters.PlaybookName) { $playbookData.parameters.PlaybookName.defaultValue }elseif ($playbookData.parameters."Playbook Name") { $playbookData.parameters."Playbook Name".defaultValue }) + + $fileName = Split-path -Parent $file | Split-Path -leaf + if ($contentToImport.Metadata) { + $baseMainTemplate.variables | Add-Member -NotePropertyName $fileName -NotePropertyValue $fileName + $baseMainTemplate.variables | Add-Member -NotePropertyName "_$fileName" -NotePropertyValue "[variables('$fileName')]" + } + + $IsLogicAppsCustomConnector = ($playbookData.resources | Where-Object {($_.type.ToLower() -eq "Microsoft.Web/customApis".ToLower())}) ? $true : $false; + + $DependencyCriteria += [PSCustomObject]@{ + kind = $IsLogicAppsCustomConnector ? "LogicAppsCustomConnector" : "Playbook";; + contentId = "[variables('_$fileName')]"; + version = "[variables('playbookVersion$playbookCounter')]"; + }; + + if (!$playbookName) { + $playbookName = $fileName; + } + + if ($playbookCounter -eq 1) { + # If a playbook exists, add CreateUIDefinition step before playbook elements while handling first playbook. + $playbookStep = [PSCustomObject] @{ + name = "playbooks"; + label = "Playbooks"; + subLabel = [PSCustomObject] @{ + preValidation = "Configure the playbooks"; + postValidation = "Done"; + }; + bladeTitle = "Playbooks"; + elements = @( + [PSCustomObject] @{ + name = "playbooks-text"; + type = "Microsoft.Common.TextBlock"; + options = [PSCustomObject] @{ + text = $contentToImport.PlaybooksBladeDescription ? $contentToImport.PlaybooksBladeDescription : "This solution installs the following Playbook templates. After installing the solution, playbooks can be managed in the Manage solution view. "; + } + }, + [PSCustomObject] @{ + name = "playbooks-link"; + type = "Microsoft.Common.TextBlock"; + options = [PSCustomObject] @{ + link = [PSCustomObject] @{ + label = "Learn more"; + uri = "https://docs.microsoft.com/azure/sentinel/tutorial-respond-threats-playbook?WT.mc_id=Portal-Microsoft_Azure_CreateUIDef" + } + } + }) + } + $baseCreateUiDefinition.parameters.steps += $playbookStep + } + $playbookDescriptionText = $(if ($contentToImport.PlaybookDescription -and $contentToImport.PlaybookDescription -is [System.Array]) { $contentToImport.PlaybookDescription[$playbookCounter - 1] } elseif ($contentToImport.PlaybookDescription -and $contentToImport.PlaybookDescription -is [System.String]) { $contentToImport.PlaybookDescription } else { "" }) + $playbookElement = [PSCustomObject] @{ + name = "playbook$playbookCounter"; + type = "Microsoft.Common.Section"; + label = $playbookName; + elements = @( + [PSCustomObject] @{ + name = "playbook$playbookCounter-text"; + type = "Microsoft.Common.TextBlock"; + options = [PSCustomObject] @{ text = if ($playbookData.metadata -and $playbookData.metadata.comments) { $playbookData.metadata.comments } else { "This playbook ingests events from $solutionName into Log Analytics using the API." } } + } + ) + } + $currentStepNum = $baseCreateUiDefinition.parameters.steps.Count - 1 + $baseCreateUiDefinition.parameters.steps[$currentStepNum].elements += $playbookElement + + foreach ($param in $playbookData.parameters.PsObject.Properties) { + $paramName = $param.Name + $defaultParamValue = $(if ($playbookData.parameters.$paramName.defaultValue) { $playbookData.parameters.$paramName.defaultValue } else { "" }) + if ($param.Name.ToLower().contains("playbookname")) { + $playbookNameObject = [PSCustomObject] @{ + name = $contentToImport.TemplateSpec ? $paramName : "playbook$playbookCounter-$paramName"; + type = "Microsoft.Common.TextBox"; + label = "Playbook Name"; + defaultValue = $defaultParamValue; + toolTip = "Resource name for the logic app playbook. No spaces are allowed"; + constraints = [PSCustomObject] @{ + required = $true; + regex = "[a-z0-9A-Z]{1,256}$"; + validationMessage = "Please enter a playbook resource name" + } + } + $baseCreateUiDefinition.parameters.steps[$currentStepNum].elements[$baseCreateUiDefinition.parameters.steps[$currentStepNum].elements.Length - 1].elements += $playbookNameObject + if(!$contentToImport.TemplateSpec) + { + $baseMainTemplate.parameters | Add-Member -NotePropertyName "playbook$playbookCounter-$paramName" -NotePropertyValue ([PSCustomObject] @{ + defaultValue = $playbookName; + type = "string"; + minLength = 1; + metadata = [PSCustomObject] @{ description = "Resource name for the logic app playbook. No spaces are allowed"; } + }) + } + } + elseif ($param.Name.ToLower().contains("username")) { + $playbookUsernameObject = [PSCustomObject] @{ + name = $contentToImport.TemplateSpec ? $paramName : "playbook$playbookCounter-$paramName"; + type = "Microsoft.Common.TextBox"; + label = "$solutionName Username"; + defaultValue = $defaultParamValue; + toolTip = "Username to connect to $solutionName API"; + constraints = [PSCustomObject] @{ + required = $true; + regex = "[a-z0-9A-Z]{1,256}$"; + validationMessage = "Please enter a playbook username"; + } + } + $baseCreateUiDefinition.parameters.steps[$currentStepNum].elements[$baseCreateUiDefinition.parameters.steps[$currentStepNum].elements.Length - 1].elements += $playbookUsernameObject + if(!$contentToImport.TemplateSpec){ + $baseMainTemplate.parameters | Add-Member -NotePropertyName "playbook$playbookCounter-$paramName" -NotePropertyValue ([PSCustomObject] @{ + defaultValue = $defaultParamValue; + type = "string"; + minLength = 1; + metadata = [PSCustomObject] @{ description = "Username to connect to $solutionName API" } + }) + } + } + elseif ($param.Name.ToLower().contains("password")) { + $playbookPasswordObject = [PSCustomObject] @{ + name = $contentToImport.TemplateSpec ? $paramName : "playbook$playbookCounter-$paramName"; + type = "Microsoft.Common.PasswordBox"; + label = [PSCustomObject] @{ password = $defaultParamValue; }; + toolTip = "Password to connect to $solutionName API"; + constraints = [PSCustomObject] @{ required = $true; }; + options = [PSCustomObject] @{ hideConfirmation = $false; }; + } + $baseCreateUiDefinition.parameters.steps[$currentStepNum].elements[$baseCreateUiDefinition.parameters.steps[$currentStepNum].elements.Length - 1].elements += $playbookPasswordObject + if(!$contentToImport.TemplateSpec){ + $baseMainTemplate.parameters | Add-Member -NotePropertyName "playbook$playbookCounter-$paramName" -NotePropertyValue ([PSCustomObject] @{ + type = "securestring"; + minLength = 1; + metadata = [PSCustomObject] @{ description = "Password to connect to $solutionName API"; } + }) + } + } + elseif ($param.Name.ToLower().contains("apikey")) { + $playbookPasswordObject = [PSCustomObject] @{ + name = $contentToImport.TemplateSpec ? $paramName : "playbook$playbookCounter-$paramName"; + type = "Microsoft.Common.PasswordBox"; + label = [PSCustomObject] @{password = "ApiKey" }; + toolTip = "ApiKey to connect to $solutionName API"; + constraints = [PSCustomObject] @{ required = $true; }; + options = [PSCustomObject] @{ hideConfirmation = $true; }; + } + $baseCreateUiDefinition.parameters.steps[$currentStepNum].elements[$baseCreateUiDefinition.parameters.steps[$currentStepNum].elements.Length - 1].elements += $playbookPasswordObject + if(!$contentToImport.TemplateSpec) + { + $baseMainTemplate.parameters | Add-Member -NotePropertyName "playbook$playbookCounter-$paramName" -NotePropertyValue ([PSCustomObject] @{ + type = "securestring"; + minLength = 1; + metadata = [PSCustomObject] @{ description = "ApiKey to connect to $solutionName API"; } + }) + } + } + else { + function PascalSplit ($pascalStr) { + foreach ($piece in $pascalStr) { + if ($piece -is [array]) { + foreach ($subPiece in $piece) { PascalSplit $subPiece } + } + else { + ($piece.ToString() -creplace '[A-Z]', ' $&').Trim().Split($null) + } + } + } + + $playbookParamObject = $( + if ($playbookData.parameters.$paramName.allowedValues) { + [PSCustomObject] @{ + name = $contentToImport.TemplateSpec ? $paramName : "playbook$playbookCounter-$paramName"; + type = "Microsoft.Common.DropDown"; + label = "$(PascalSplit $paramName)"; + placeholder = "$($playbookData.parameters.$paramName.allowedValues[0])"; + defaultValue = "$($playbookData.parameters.$paramName.allowedValues[0])"; + toolTip = "Please enter $(if($paramName.IndexOf("-") -ne -1){$paramName}else{PascalSplit $paramName})"; + constraints = [PSCustomObject] @{ + allowedValues = $playbookData.parameters.$paramName.allowedValues | ForEach-Object { + [PSCustomObject] @{ + label = $_; + value = $_; + } + } + required = $true; + } + visible = $true; + } + } + else { + [PSCustomObject] @{ + name = $contentToImport.TemplateSpec ? $paramName : "playbook$playbookCounter-$paramName"; + type = "Microsoft.Common.TextBox"; + label = "$(PascalSplit $paramName)"; + defaultValue = $defaultParamValue; + toolTip = "Please enter $(if($paramName.IndexOf("-") -ne -1){$paramName}else{PascalSplit $paramName})"; + constraints = [PSCustomObject] @{ + required = $true; + regex = "[a-z0-9A-Z]{1,256}$"; + validationMessage = "Please enter the $(PascalSplit $paramName)" + } + } + } + ) + $baseCreateUiDefinition.parameters.steps[$currentStepNum].elements[$baseCreateUiDefinition.parameters.steps[$currentStepNum].elements.Length - 1].elements += $playbookParamObject + $defaultValue = $(if ($defaultParamValue) { $defaultParamValue } else { "" }) + if(!$contentToImport.TemplateSpec){ + $baseMainTemplate.parameters | Add-Member -NotePropertyName "playbook$playbookCounter-$paramName" -NotePropertyValue ([PSCustomObject] @{ + defaultValue = $defaultValue; + type = "string"; + minLength = 1; + }) + } + } + if(!$contentToImport.TemplateSpec){ + $baseCreateUiDefinition.parameters.outputs | Add-Member -NotePropertyName "playbook$playbookCounter-$paramName" -NotePropertyValue "[steps('playbooks').playbook$playbookCounter.playbook$playbookCounter-$paramName]" + } + } + + foreach ($playbookVariable in $playbookData.variables.PsObject.Properties) { + $variableName = $playbookVariable.Name + $variableValue = $playbookVariable.Value + if ($variableValue -is [System.String]) { + $variableValue = $(node "$PSScriptRoot/templating/replacePlaybookParamNames.js" $variableValue $playbookCounter) + } + if($contentToImport.TemplateSpec -and $variableName.ToLower().Contains("connection")) + { + $variableValue = "[" + $variableValue ; + } + if (!$contentToImport.TemplateSpec -and $variableName.ToLower().contains("apikey")) { + $baseMainTemplate.variables | Add-Member -NotePropertyName "playbook-$variableName" -NotePropertyValue "[$variableValue]" + } + elseif (!$contentToImport.TemplateSpec) { + $baseMainTemplate.variables | Add-Member -NotePropertyName "playbook$playbookCounter-$variableName" -NotePropertyValue $variableValue + } + } + + $azureManagementUrlExists = $false + $azureManagementUrl = "management.azure.com" + + function replaceQuotes ($inputStr) { + $baseStr = $resourceObj.$key + $outputStr = $baseStr.Replace("`"", "\`"") + $outputStr + } + + function removeBlanksRecursively($resourceObj) { + if ($resourceObj.GetType() -ne [System.DateTime]) { + foreach ($prop in $resourceObj.PsObject.Properties) { + $key = $prop.Name + if ($prop.Value -is [System.String]) { + #Write-Host "its a string $resourceObj.$key" + if($resourceObj.$key -eq "") + { + $resourceObj.$key = "[variables('blanks')]"; + } + } + elseif ($prop.Value -is [System.Array]) { + foreach ($item in $prop.Value) { + $itemIndex = $prop.Value.IndexOf($item) + if ($null -ne $itemIndex) { + if ($item -is [System.String]) { + $resourceObj.$key[$itemIndex] = $item + } + elseif ($item -is [System.Management.Automation.PSCustomObject]) { + $resourceObj.$key[$itemIndex] = $(removeBlanksRecursively $item) + } + } + } + } + else { + if (($prop.Value -isnot [System.Int32]) -and ($prop.Value -isnot [System.Int64])) { + $resourceObj.$key = $(removeBlanksRecursively $resourceObj.$key) + } + } + } + } + $resourceObj + } + + function addInternalSuffixRecursively($resourceObj) { + if ($resourceObj.GetType() -ne [System.DateTime]) { + foreach ($prop in $resourceObj.PsObject.Properties) { + $key = $prop.Name + if ($prop.Value -is [System.String]) { + $resourceObj.$key = $resourceObj.$key.Replace("resourceGroup().location", "variables('workspace-location-inline')") + if ($key -eq "operationId") { + $playbookData.variables | Add-Member -NotePropertyName "operationId-$($resourceobj.$key)" -NotePropertyValue $($resourceobj.$key) + $playbookData.variables | Add-Member -NotePropertyName "_operationId-$($resourceobj.$key)" -NotePropertyValue "[variables('operationId-$($resourceobj.$key)')]" + $resourceObj.$key = "[variables('_operationId-$($resourceobj.$key)')]" + } + if($contentToImport.TemplateSpec -and ($resourceObj.$key.StartsWith("["))) + { + $resourceObj.$key = "[" + $resourceObj.$key; + } + } + elseif ($prop.Value -is [System.Array]) { + foreach ($item in $prop.Value) { + $itemIndex = $prop.Value.IndexOf($item) + if ($null -ne $itemIndex) { + if ($item -is [System.String]) { + $item = $item.Replace("resourceGroup().location", "variables('workspace-location-inline')") + if($contentToImport.TemplateSpec -and ($item.StartsWith("["))) + { + $item = "[" + $item; + } + $resourceObj.$key[$itemIndex] = $item + } + elseif ($item -is [System.Management.Automation.PSCustomObject]) { + $resourceObj.$key[$itemIndex] = $(addInternalSuffixRecursively $item) + } + } + } + } + else { + if (($prop.Value -isnot [System.Int32]) -and ($prop.Value -isnot [System.Int64])) { + $resourceObj.$key = $(addInternalSuffixRecursively $resourceObj.$key) + } + } + } + } + $resourceObj + } + + function replaceVarsRecursively ($resourceObj) { + if ($resourceObj.GetType() -ne [System.DateTime]) { + foreach ($prop in $resourceObj.PsObject.Properties) { + $key = $prop.Name + if ($prop.Value -is [System.String]) { + $resourceObj.$key = $(node "$PSScriptRoot/templating/replacePlaybookParamNames.js" "$(replaceQuotes $resourceObj.$key)" $playbookCounter) + if($contentToImport.TemplateSpec -and ($resourceObj.$key.StartsWith("[") -and $resourceObj.$key.Contains("parameters(") -and !$resourceObj.$key.contains("parameters('workspace-location')"))) + { + $resourceObj.$key = "[" + $resourceObj.$key; + } + if ($resourceObj.$key.StartsWith("[") -and $resourceObj.$key[$resourceObj.$key.Length - 1] -eq "]") { + $resourceObj.$key = $(node "$PSScriptRoot/templating/replacePlaybookVarNames.js" "$(replaceQuotes $resourceObj.$key)" $playbookCounter) + } + $resourceObj.$key = $(node "$PSScriptRoot/templating/replaceLocationValue.js" "$(replaceQuotes $resourceObj.$key)" $playbookCounter) + if ($resourceObj.$key.IndexOf($azureManagementUrl)) { + $resourceObj.$key = $resourceObj.$key.Replace($azureManagementUrl, "@{variables('azureManagementUrl')}") + $azureManagementUrlExists = $true + } + if ($key -eq "operationId") { + $baseMainTemplate.variables | Add-Member -NotePropertyName "operationId-$($resourceobj.$key)" -NotePropertyValue $($resourceobj.$key) + $baseMainTemplate.variables | Add-Member -NotePropertyName "_operationId-$($resourceobj.$key)" -NotePropertyValue "[variables('operationId-$($resourceobj.$key)')]" + $resourceObj.$key = "[variables('_operationId-$($resourceobj.$key)')]" + } + } + elseif ($prop.Value -is [System.Array]) { + foreach ($item in $prop.Value) { + $itemIndex = $prop.Value.IndexOf($item) + if ($null -ne $itemIndex) { + if ($item -is [System.String]) { + $item = $(node "$PSScriptRoot/templating/replaceLocationValue.js" $item $playbookCounter) + $item = $(node "$PSScriptRoot/templating/replacePlaybookParamNames.js" $item $playbookCounter) + if($contentToImport.TemplateSpec -and ($item.StartsWith("[") -and $item.Contains("parameters(") -and !$item.contains("parameters('workspace-location')"))) + { + $item = "[" + $item; + } + if ($item.StartsWith("[") -and $item[$item.Length - 1] -eq "]") { + $item = $(node "$PSScriptRoot/templating/replacePlaybookVarNames.js" $item $playbookCounter) + } + $resourceObj.$key[$itemIndex] = $item + } + elseif ($item -is [System.Management.Automation.PSCustomObject]) { + $resourceObj.$key[$itemIndex] = $(replaceVarsRecursively $item) + } + } + } + } + else { + if (($prop.Value -isnot [System.Int32]) -and ($prop.Value -isnot [System.Int64])) { + $resourceObj.$key = $(replaceVarsRecursively $resourceObj.$key) + } + } + } + } + $resourceObj + } + $connectionCounter = 1 + function getConnectionVariableName($connectionVariable) { + foreach ($templateVar in $($baseMainTemplate.variables).PSObject.Properties) { + if ($templateVar.Value -eq $connectionVariable) { + return $templateVar.Name + } + } + return $false + } + + $playbookDependencies = @(); + $playbookResources = @(); + $playbookVersion = '1.0'; + $logicAppsPlaybookId = ''; + $customConnectorContentId = ''; + foreach ($playbookResource in $playbookData.resources) { + if ($playbookResource.type -eq "Microsoft.Web/connections") { + if ($playbookResource.properties -and $playbookResource.properties.api -and $playbookResource.properties.api.id) { + if ($playbookResource.properties.api.id.Contains("/providers/Microsoft.Web/customApis/")) { + $splits = $playbookResource.properties.api.id.Split(','); + $connectionKey = $splits[-1].Trim().Replace("parameters('","").Replace("'","").Replace(")","").Replace("]",""); + + foreach ($templateVar in $($playbookData.parameters).PSObject.Properties) { + if ($templateVar.Name -eq $connectionKey) { + $playbookDependencies += [PSCustomObject] @{ + kind = "LogicAppsCustomConnector"; + contentId = $customConnectorsList[$templateVar.Value.defaultValue].id; + version = $customConnectorsList[$templateVar.Value.defaultValue].version; + } + } + } + } + + $connectionVar = $playbookResource.properties.api.id + $connectionVar = $connectionVar.Replace("resourceGroup().location", "variables('workspace-location-inline')") + $variableReferenceString = "[variables" + $varName = "" + if ($connectionVar.StartsWith($variableReferenceString)) { + # Get value of variable + $varName = $($connectionVar.Split("'"))[1] + # Handle variable reference pairs + if ($playbookData.variables.$varName.StartsWith($variableReferenceString)) { + $varName = $($playbookData.variables.$varName.Split("'"))[1] + } + $connectionVar = $playbookData.variables.$varName + $connectionVar = $connectionVar.Replace("resourceGroup().location", "variables('workspace-location-inline')") + + } + $foundConnection = getConnectionVariableName $connectionVar + if ($foundConnection) { + $playbookResource.properties.api.id = "[variables('_$foundConnection')]" + } + else { + $playbookData.variables | Add-Member -NotePropertyName "connection-$connectionCounter" -NotePropertyValue $connectionVar + $playbookData.variables | Add-Member -NotePropertyName "_connection-$connectionCounter" -NotePropertyValue "[variables('connection-$connectionCounter')]" + $playbookResource.properties.api.id = "[variables('_connection-$connectionCounter')]" + } + if(($playbookResource.properties.parameterValues) -and ($null -ne $baseMainTemplate.variables.'playbook-ApiKey')) { + $playbookResource.properties.parameterValues.api_key = "[variables('playbook-ApiKey')]" + } + } + } + elseif ($contentToImport.TemplateSpec -and $playbookResource.type -eq "Microsoft.Logic/workflows") { + if($null -eq $playbookResource.tags) + { + $playbookResource | Add-Member -NotePropertyName "tags" -NotePropertyValue ([PSCustomObject]@{}); + } + $playbookResource.tags | Add-Member -NotePropertyName "hidden-SentinelWorkspaceId" -NotePropertyValue "[variables('workspaceResourceId')]"; + $playbookVersion = $playbookResource.tags.'hidden-SentinelTemplateVersion' ? $playbookResource.tags.'hidden-SentinelTemplateVersion' : $playbookVersion; + } elseif ($contentToImport.TemplateSpec -and $playbookResource.type -eq "Microsoft.Web/customApis") { + $logicAppsPlaybookId = $playbookResource.name.Replace("parameters('","").Replace("'","").Replace(")","").Replace("]","").Replace("[",""); + $customConnectorContentId = $playbookData.parameters.$logicAppsPlaybookId.defaultValue + Write-Host $customConnectorContentId; + Write-Host $logicAppsPlaybookId; + } + + + $playbookResource = $playbookResource + $playbookResource = $(removePropertiesRecursively $playbookResource) + $playbookResource = $(addInternalSuffixRecursively $playbookResource) + $playbookResource = $(removeBlanksRecursively $playbookResource) + $playbookResources += $playbookResource; + $connectionCounter += 1 + } + + if($contentToImport.TemplateSpec) + { + $baseMainTemplate.variables | Add-Member -NotePropertyName "playbookVersion$playbookCounter" -NotePropertyValue $playbookVersion + $baseMainTemplate.variables | Add-Member -NotePropertyName "playbookContentId$playbookCounter" -NotePropertyValue $fileName + $baseMainTemplate.variables | Add-Member -NotePropertyName "_playbookContentId$playbookCounter" -NotePropertyValue "[variables('playbookContentId$playbookCounter')]" + + if (!$IsLogicAppsCustomConnector) { + $baseMainTemplate.variables | Add-Member -NotePropertyName "playbookId$playbookCounter" -NotePropertyValue "[resourceId('Microsoft.Logic/workflows', variables('playbookContentId$playbookCounter'))]" + } + + $baseMainTemplate.variables | Add-Member -NotePropertyName "playbookTemplateSpecName$playbookCounter" -NotePropertyValue ($IsLogicAppsCustomConnector ? "[concat(parameters('workspace'),'-lc-',uniquestring(variables('_playbookContentId$playbookCounter')))]" : "[concat(parameters('workspace'),'-pl-',uniquestring(variables('_playbookContentId$playbookCounter')))]") + # Add workspace resource ID if not available + if (!$baseMainTemplate.variables.workspaceResourceId) { + $baseMainTemplate.variables | Add-Member -NotePropertyName "workspaceResourceId" -NotePropertyValue "[resourceId('microsoft.OperationalInsights/Workspaces', parameters('workspace'))]" + } + # Add base templateSpec + $basePlaybookTemplateSpec = [PSCustomObject]@{ + type = "Microsoft.Resources/templateSpecs"; + apiVersion = "2021-05-01"; + name = "[variables('playbookTemplateSpecName$playbookCounter')]"; + location = "[parameters('workspace-location')]"; + tags = [PSCustomObject]@{ + "hidden-sentinelWorkspaceId" = "[variables('workspaceResourceId')]"; + "hidden-sentinelContentType" = $IsLogicAppsCustomConnector ? "LogicAppsCustomConnector" : "Playbook"; + }; + properties = [PSCustomObject]@{ + description = $IsLogicAppsCustomConnector ? $playbookName : "$($playbookName) playbook"; + displayName = $IsLogicAppsCustomConnector ? $playbookName : "$($playbookName) playbook"; + } + } + + $baseMainTemplate.resources += $basePlaybookTemplateSpec + $author = $contentToImport.Author.Split(" - "); + $authorDetails = [PSCustomObject]@{ + name = $author[0]; + }; + if($null -ne $author[1]) + { + $authorDetails | Add-Member -NotePropertyName "email" -NotePropertyValue "[variables('_email')]" + } + if ($IsLogicAppsCustomConnector) { + $customConnectorsList.add($customConnectorContentId, @{ id="[variables('_$filename')]"; version="[variables('playbookVersion$playbookCounter')]"}); + } + + $playbookMetadata = [PSCustomObject]@{ + type = "Microsoft.OperationalInsights/workspaces/providers/metadata"; + apiVersion = "2022-01-01-preview"; + name = $IsLogicAppsCustomConnector ? "[[concat(variables('workspace-name'),'/Microsoft.SecurityInsights/',concat('LogicAppsCustomConnector-', last(split(variables('playbookId'),'/'))))]" : "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('Playbook-', last(split(variables('playbookId$playbookCounter'),'/'))))]"; + properties = [PSCustomObject]@{ + parentId = $IsLogicAppsCustomConnector ? "[[variables('playbookId')]" : "[variables('playbookId$playbookCounter')]" + contentId = "[variables('_playbookContentId$playbookCounter')]"; + kind = $IsLogicAppsCustomConnector ? "LogicAppsCustomConnector" : "Playbook"; + version = "[variables('playbookVersion$playbookCounter')]"; + source = [PSCustomObject]@{ + kind = "Solution"; + name = $contentToImport.Name; + sourceId = "[variables('_solutionId')]" + }; + author = $authorDetails; + support = $baseMetadata.support + } + } + + if ($playbookDependencies) { + $criteria = [PSCustomObject]@{ + criteria = $playbookDependencies + }; + $playbookMetadata.properties | Add-Member -NotePropertyName "dependencies" -NotePropertyValue $criteria + } + + $playbookVariables = [PSCustomObject]@{}; + foreach($var in $playbookData.variables.PsObject.Properties) + { + $playbookVariables | Add-Member -NotePropertyName $var.Name -NotePropertyValue $(($contentToImport.TemplateSpec -and $var.Value.StartsWith("[")) ? "[" + $var.Value : $var.Value); + } + + $playbookVariables | Add-Member -NotePropertyName "workspace-location-inline" -NotePropertyValue "[concat('[resourceGroup().locatio', 'n]')]"; + if ($IsLogicAppsCustomConnector) { + $playbookVariables | Add-Member -NotePropertyName "playbookContentId" -NotePropertyValue $fileName; + $playbookVariables | Add-Member -NotePropertyName "playbookId" -NotePropertyValue "[[resourceId('Microsoft.Web/customApis', parameters('$logicAppsPlaybookId'))]" + } + $playbookVariables | Add-Member -NotePropertyName "workspace-name" -NotePropertyValue "[parameters('workspace')]" + $playbookVariables | Add-Member -NotePropertyName "workspaceResourceId" -NotePropertyValue "[[resourceId('microsoft.OperationalInsights/Workspaces', variables('workspace-name'))]" + $playbookResources = $playbookResources + $playbookMetadata; + + $playbookMetadata = [PSCustomObject]@{}; + foreach($var in $playbookData.metadata.PsObject.Properties) + { + $playbookMetadata | Add-Member -NotePropertyName $var.Name -NotePropertyValue $var.Value; + } + + # Add templateSpecs/versions resource to hold actual content + $playbookTemplateSpecContent = [PSCustomObject]@{ + type = "Microsoft.Resources/templateSpecs/versions"; + apiVersion = "2021-05-01"; + name = "[concat(variables('playbookTemplateSpecName$playbookCounter'),'/',variables('playbookVersion$playbookCounter'))]"; + location = "[parameters('workspace-location')]"; + tags = [PSCustomObject]@{ + "hidden-sentinelWorkspaceId" = "[variables('workspaceResourceId')]"; + "hidden-sentinelContentType" = $IsLogicAppsCustomConnector ? "LogicAppsCustomConnector" : "Playbook"; + }; + dependsOn = @( + "[resourceId('Microsoft.Resources/templateSpecs', variables('playbookTemplateSpecName$playbookCounter'))]" + ); + properties = [PSCustomObject]@{ + description = "$($playbookName) Playbook with template version $($contentToImport.Version)"; + mainTemplate = [PSCustomObject]@{ + '$schema' = "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#"; + contentVersion = "[variables('playbookVersion$playbookCounter')]"; + parameters = $playbookData.parameters; + variables = $playbookVariables; + resources = $playbookResources; + } + } + } + + if (@($playbookMetadata.PsObject.Properties).Count -gt 0) { + Write-Host "creating metadata for playbook" + $playbookTemplateSpecContent.properties.mainTemplate | Add-Member -NotePropertyName "metadata" -NotePropertyValue ([PSCustomObject]@{}); + foreach($var in $playbookMetadata.PsObject.Properties) { + if ($var.Name -ne "author" -and $var.Name -ne "support" -and $var.Name -ne "prerequisitesDeployTemplateFile") { + $playbookTemplateSpecContent.properties.mainTemplate.metadata | Add-Member -NotePropertyName $var.Name -NotePropertyValue $var.Value; + } + } + if (!$playbookTemplateSpecContent.properties.mainTemplate.metadata.'lastUpdateTime') { + $playbookTemplateSpecContent.properties.mainTemplate.metadata | Add-Member -NotePropertyName "lastUpdateTime" -NotePropertyValue (get-date -format "yyyy-MM-ddTHH:mm:ss.fffZ"); + } + } + if (!$playbookMetadata.releaseNotes -and $playbookTemplateSpecContent.properties.mainTemplate.metadata) { + Write-Host "adding default release notes" + $releaseNotes = [PSCustomObject]@{ + version = $playbookVersion; + title = "[variables('blanks')]"; + notes = @("Initial version"); + } + $playbookTemplateSpecContent.properties.mainTemplate.metadata | Add-Member -NotePropertyName 'releaseNotes' -NotePropertyValue $releaseNotes; + if (!$baseMainTemplate.variables.blanks) { + $baseMainTemplate.variables | Add-Member -NotePropertyName "blanks" -NotePropertyValue "[replace('b', 'b', '')]" + } + } + + $baseMainTemplate.resources += $playbookTemplateSpecContent; + } + else + { + $baseMainTemplate.resources += $playbookResources; + } + + if ($azureManagementUrlExists) { + $baseMainTemplate.variables | Add-Member -NotePropertyName "azureManagementUrl" -NotePropertyValue $azureManagementUrl + } + + $playbookCounter += 1 + } + elseif ($objectKeyLowercase -eq "data connectors") { + Write-Host "Generating Data Connector using $file" + try { + $connectorData = ConvertFrom-Json $rawData + } + catch { + Write-Host "Failed to deserialize $file" -ForegroundColor Red + break; + } + $connectorNameParamObj = [PSCustomObject] @{ + type = "string"; + defaultValue = $(New-Guid).Guid + } + $connectorId = $connectorData.id + 'Connector'; + if ($contentToImport.Metadata -and !$contentToImport.TemplateSpec) { + $baseMainTemplate.variables | Add-Member -NotePropertyName $connectorId -NotePropertyValue $connectorId + $baseMainTemplate.variables | Add-Member -NotePropertyName "_$connectorId" -NotePropertyValue "[variables('$connectorId')]" + } + if (!$contentToImport.TemplateSpec){ + $baseMainTemplate.parameters | Add-Member -NotePropertyName "connector$connectorCounter-name" -NotePropertyValue $connectorNameParamObj + $baseMainTemplate.variables | Add-Member -NotePropertyName "connector$connectorCounter-source" -NotePropertyValue "[concat('/subscriptions/',subscription().subscriptionId,'/resourceGroups/',resourceGroup().name,'/providers/Microsoft.OperationalInsights/workspaces/',parameters('workspace'),'/providers/Microsoft.SecurityInsights/dataConnectors/',parameters('connector$connectorCounter-name'))]" + $baseMainTemplate.variables | Add-Member -NotePropertyName "_connector$connectorCounter-source" -NotePropertyValue "[variables('connector$connectorCounter-source')]" + }; + $DependencyCriteria += [PSCustomObject]@{ + kind = "DataConnector"; + contentId = if ($contentToImport.TemplateSpec){"[variables('_dataConnectorContentId$connectorCounter')]"}else{"[variables('_$connectorId')]"}; + version = if ($contentToImport.TemplateSpec){"[variables('dataConnectorVersion$connectorCounter')]"}else{$contentToImport.Version}; + }; + foreach ($step in $connectorData.instructionSteps) { + # Remove empty properties from each instructionStep + $stepIndex = $connectorData.instructionSteps.IndexOf($step) + $connectorData.instructionSteps[$stepIndex] = handleEmptyInstructionProperties $step + } + + if ($contentToImport.TemplateSpec) { + $connectorName = $contentToImport.Name + # Add workspace resource ID if not available + if (!$baseMainTemplate.variables.workspaceResourceId) { + $baseMainTemplate.variables | Add-Member -NotePropertyName "workspaceResourceId" -NotePropertyValue "[resourceId('microsoft.OperationalInsights/Workspaces', parameters('workspace'))]" + } + # If both ID and Title exist, is standard GenericUI data connector + if ($connectorData.id -and $connectorData.title) { + $baseMainTemplate.variables | Add-Member -NotePropertyName "uiConfigId$connectorCounter" -NotePropertyValue $connectorData.id + $baseMainTemplate.variables | Add-Member -NotePropertyName "_uiConfigId$connectorCounter" -NotePropertyValue "[variables('uiConfigId$connectorCounter')]" + } + $baseMainTemplate.variables | Add-Member -NotePropertyName "dataConnectorContentId$connectorCounter" -NotePropertyValue $connectorData.id + $baseMainTemplate.variables | Add-Member -NotePropertyName "_dataConnectorContentId$connectorCounter" -NotePropertyValue "[variables('dataConnectorContentId$connectorCounter')]" + $baseMainTemplate.variables | Add-Member -NotePropertyName "dataConnectorId$connectorCounter" -NotePropertyValue "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/dataConnectors', variables('_dataConnectorContentId$connectorCounter'))]" + $baseMainTemplate.variables | Add-Member -NotePropertyName "_dataConnectorId$connectorCounter" -NotePropertyValue "[variables('dataConnectorId$connectorCounter')]" + $baseMainTemplate.variables | Add-Member -NotePropertyName "dataConnectorTemplateSpecName$connectorCounter" -NotePropertyValue "[concat(parameters('workspace'),'-dc-',uniquestring(variables('_dataConnectorContentId$connectorCounter')))]" + $baseMainTemplate.variables | Add-Member -NotePropertyName "dataConnectorVersion$connectorCounter" -NotePropertyValue (($null -ne $connectorData.metadata) ? "$($connectorData.metadata.version)" : "1.0.0") + if (!$contentToImport.TemplateSpec){ + $baseMainTemplate.variables | Add-Member -NotePropertyName "parentId" -NotePropertyValue $solutionId + $baseMainTemplate.variables | Add-Member -NotePropertyName "_parentId" -NotePropertyValue "[variables('parentId')]" + }; + + # Add base templateSpec + $baseDataConnectorTemplateSpec = [PSCustomObject]@{ + type = "Microsoft.Resources/templateSpecs"; + apiVersion = "2021-05-01"; + name = "[variables('dataConnectorTemplateSpecName$connectorCounter')]"; + location = "[parameters('workspace-location')]"; + tags = [PSCustomObject]@{ + "hidden-sentinelWorkspaceId" = "[variables('workspaceResourceId')]"; + "hidden-sentinelContentType" = "DataConnector"; + }; + properties = [PSCustomObject]@{ + description = "$($connectorName) data connector with template"; + displayName = "$($connectorName) template"; + } + } + $baseMainTemplate.resources += $baseDataConnectorTemplateSpec + + if(!$contentToImport.Is1PConnector) + { + $existingFunctionApp = $false; + $instructionArray = $connectorData.instructionSteps + ($instructionArray | ForEach {if($_.description -and $_.description.IndexOf('[Deploy To Azure]') -gt 0){$existingFunctionApp = $true;}}) + if($existingFunctionApp) + { + $connectorData.title = $connectorData.title + " (using Azure Function)"; + } + } + # Data Connector Content -- *Assumes GenericUI + if($contentToImport.Is1PConnector) + { + $1pconnectorData = $connectorData + $1pconnectorData = $1pconnectorData | Select-Object -Property id,title,publisher,descriptionMarkdown, graphQueries, connectivityCriterias,dataTypes + } + $templateSpecConnectorUiConfig = ($contentToImport.Is1PConnector -eq $true) ? $1pconnectorData : $connectorData + $templateSpecConnectorUiConfig.id = "[variables('_uiConfigId$connectorCounter')]" + if($contentToImport.Is1PConnector -eq $false) + { + $templateSpecConnectorUiConfig.availability.isPreview = ($templateSpecConnectorUiConfig.availability.isPreview -eq $true) ? $false : $templateSpecConnectorUiConfig.availability.isPreview + } + $dataConnectorContent = [PSCustomObject]@{ + name = "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',variables('_dataConnectorContentId$connectorCounter'))]"; + apiVersion = "2021-03-01-preview"; + type = "Microsoft.OperationalInsights/workspaces/providers/dataConnectors"; + location = "[parameters('workspace-location')]"; + kind = ($contentToImport.Is1PConnector -eq $true) ? "StaticUI" : "GenericUI"; + properties = [PSCustomObject]@{ + connectorUiConfig = $templateSpecConnectorUiConfig + } + } + $author = $contentToImport.Author.Split(" - "); + $authorDetails = [PSCustomObject]@{ + name = $author[0]; + }; + if($null -ne $author[1]) + { + $authorDetails | Add-Member -NotePropertyName "email" -NotePropertyValue "[variables('_email')]" + } + $dataConnectorMetadata = [PSCustomObject]@{ + type = "Microsoft.OperationalInsights/workspaces/providers/metadata"; + apiVersion = "2022-01-01-preview"; + name = "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('DataConnector-', last(split(variables('_dataConnectorId$connectorCounter'),'/'))))]"; + properties = [PSCustomObject]@{ + parentId = "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/dataConnectors', variables('_dataConnectorContentId$connectorCounter'))]"; + contentId = "[variables('_dataConnectorContentId$connectorCounter')]"; + kind = "DataConnector"; + version = "[variables('dataConnectorVersion$connectorCounter')]"; + source = [PSCustomObject]@{ + kind = "Solution"; + name = $contentToImport.Name; + sourceId = "[variables('_solutionId')]" + }; + author = $authorDetails; + support = $baseMetadata.support + } + } + # Add templateSpecs/versions resource to hold actual content + $dataConnectorTemplateSpecContent = [PSCustomObject]@{ + type = "Microsoft.Resources/templateSpecs/versions"; + apiVersion = "2021-05-01"; + name = "[concat(variables('dataConnectorTemplateSpecName$connectorCounter'),'/',variables('dataConnectorVersion$connectorCounter'))]"; + location = "[parameters('workspace-location')]"; + tags = [PSCustomObject]@{ + "hidden-sentinelWorkspaceId" = "[variables('workspaceResourceId')]"; + "hidden-sentinelContentType" = "DataConnector"; + }; + dependsOn = @( + "[resourceId('Microsoft.Resources/templateSpecs', variables('dataConnectorTemplateSpecName$connectorCounter'))]" + ); + properties = [PSCustomObject]@{ + description = "$($connectorName) data connector with template version $($contentToImport.Version)"; + mainTemplate = [PSCustomObject]@{ + '$schema' = "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#"; + contentVersion = "[variables('dataConnectorVersion$connectorCounter')]"; + parameters = [PSCustomObject]@{}; + variables = [PSCustomObject]@{}; + resources = @( + # Data Connector + $dataConnectorContent, + # Metadata + $dataConnectorMetadata + ) + } + } + } + $baseMainTemplate.resources += $dataConnectorTemplateSpecContent + + # Add content-metadata item, in addition to template spec metadata item + $dataConnectorActiveContentMetadata = [PSCustomObject]@{ + type = "Microsoft.OperationalInsights/workspaces/providers/metadata"; + apiVersion = "2022-01-01-preview"; + name = "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('DataConnector-', last(split(variables('_dataConnectorId$connectorCounter'),'/'))))]"; + dependsOn = @("[variables('_dataConnectorId$connectorCounter')]"); + location = "[parameters('workspace-location')]"; + properties = [PSCustomObject]@{ + parentId = "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/dataConnectors', variables('_dataConnectorContentId$connectorCounter'))]"; + contentId = "[variables('_dataConnectorContentId$connectorCounter')]"; + kind = "DataConnector"; + version = "[variables('dataConnectorVersion$connectorCounter')]"; + source = [PSCustomObject]@{ + kind = "Solution"; + name = $contentToImport.Name; + sourceId = "[variables('_solutionId')]" + }; + author = $authorDetails; + support = $baseMetadata.support + } + } + $baseMainTemplate.resources += $dataConnectorActiveContentMetadata + } + $connectorObj = [PSCustomObject]@{} + # If direct title is available, assume standard connector format + if ($connectorData.title) { + $standardConnectorUiConfig = [PSCustomObject]@{ + title = $connectorData.title; + publisher = $connectorData.publisher; + descriptionMarkdown = $connectorData.descriptionMarkdown; + graphQueries = $connectorData.graphQueries; + dataTypes = $connectorData.dataTypes; + connectivityCriterias = $connectorData.connectivityCriterias; + } + + if(!$contentToImport.Is1PConnector) + { + $standardConnectorUiConfig | Add-Member -NotePropertyName "sampleQueries" -NotePropertyValue $connectorData.sampleQueries; + $standardConnectorUiConfig | Add-Member -NotePropertyName "availability" -NotePropertyValue $connectorData.availability; + $standardConnectorUiConfig | Add-Member -NotePropertyName "permissions" -NotePropertyValue $connectorData.permissions; + $standardConnectorUiConfig | Add-Member -NotePropertyName "instructionSteps" -NotePropertyValue $connectorData.instructionSteps; + } + + if($contentToImport.TemplateSpec){ + $standardConnectorUiConfig | Add-Member -NotePropertyName "id" -NotePropertyValue "[variables('_uiConfigId$connectorCounter')]" + + } + + $connectorObj = [PSCustomObject]@{ + name = if ($contentToImport.TemplateSpec) { "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',variables('_dataConnectorContentId$connectorCounter'))]" }else { "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',parameters('connector$connectorCounter-name'))]" } + apiVersion = "2021-03-01-preview"; + type = "Microsoft.OperationalInsights/workspaces/providers/dataConnectors"; + location = "[parameters('workspace-location')]"; + kind = ($contentToImport.Is1PConnector -eq $true) ? "StaticUI" : "GenericUI"; + properties = [PSCustomObject]@{ + connectorUiConfig = $standardConnectorUiConfig + } + } + + if(!$contentToImport.TemplateSpec) + { + $connectorObj | Add-Member -NotePropertyName "id" -NotePropertyValue "[variables('_connector$connectorCounter-source')]"; + } + } + elseif ($connectorData.resources -and + $connectorData.resources[0] -and + $connectorData.resources[0].properties -and + $connectorData.resources[0].properties.connectorUiConfig -and + $connectorData.resources[0].properties.pollingConfig) { + # Else check if Polling connector + $connectorData = $connectorData.resources[0] + $connectorUiConfig = $connectorData.properties.connectorUiConfig + $connectorUiConfig.PSObject.Properties.Remove('id') + $connectorObj = [PSCustomObject]@{ + id = if ($contentToImport.TemplateSpec) { "[variables('_uiConfigId$connectorCounter')]" }else { "[variables('_connector$connectorCounter-source')]" }; + name = if ($contentToImport.TemplateSpec) { "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',variables('_dataConnectorContentId$connectorCounter'))]" }else { "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',parameters('connector$connectorCounter-name'))]" } + apiVersion = "2021-03-01-preview"; + type = "Microsoft.OperationalInsights/workspaces/providers/dataConnectors"; + location = "[parameters('workspace-location')]"; + kind = $connectorData.kind; + properties = [PSCustomObject]@{ + connectorUiConfig = $connectorUiConfig; + pollingConfig = $connectorData.properties.pollingConfig; + } + } + } + if ($connectorData.additionalRequirementBanner) { + $connectorObj.properties.connectorUiConfig | Add-Member -NotePropertyName "additionalRequirementBanner" -NotePropertyValue $connectorData.additionalRequirementBanner + } + + $baseMainTemplate.resources += $connectorObj + + $syslog = "Syslog" + $commonSecurityLog = "CommonSecurityLog" + function getConnectorDataTypes($dataTypesArray) { + $typeResult = "custom log" + foreach ($dataType in $dataTypesArray) { + if ($dataType.name.IndexOf($syslog) -ne -1) { + $typeResult = $syslog + } + elseif ($dataType.name.IndexOf($commonSecurityLog) -ne -1) { + $typeResult = $commonSecurityLog + } + } + return $typeResult + } + function getAllDataTypeNames($dataTypesArray) { + $typeResult = @() + foreach ($dataType in $dataTypesArray) { + $typeResult += $dataType.name + } + return $typeResult + } + $connectorDataType = $(getConnectorDataTypes $connectorData.dataTypes) + $isParserAvailable = $($contentToImport.Parsers -and ($contentToImport.Parsers.Count -gt 0)) + $baseDescriptionText = "This Solution installs the data connector for $solutionName. You can get $solutionName $connectorDataType data in your Microsoft Sentinel workspace. Configure and enable this data connector in the Data Connector gallery after this Solution deploys." + $parserText = "The Solution installs a parser that transforms the ingested data into Microsoft Sentinel normalized format. The normalized format enables better correlation of different types of data from different data sources to drive end-to-end outcomes seamlessly in security monitoring, hunting, incident investigation and response scenarios in Microsoft Sentinel." + $customLogsText = "$baseDescriptionText This data connector creates custom log table(s) $(getAllDataTypeNames $connectorData.dataTypes) in your Microsoft Sentinel / Azure Log Analytics workspace." + $syslogText = "$baseDescriptionText The logs will be received in the Syslog table in your Microsoft Sentinel / Azure Log Analytics workspace." + $commonSecurityLogText = "$baseDescriptionText The logs will be received in the CommonSecurityLog table in your Microsoft Sentinel / Azure Log Analytics workspace." + $connectorDescriptionText = $(if ($connectorDataType -eq $commonSecurityLog) { $commonSecurityLogText } elseif ($connectorDataType -eq $syslog) { $syslogText } else { $customLogsText }) + + $baseDataConnectorStep = [PSCustomObject] @{ + name = "dataconnectors"; + label = "Data Connectors"; + bladeTitle = "Data Connectors"; + elements = @(); + } + $baseDataConnectorTextElement = [PSCustomObject] @{ + name = "dataconnectors$connectorCounter-text"; + type = "Microsoft.Common.TextBlock"; + options = [PSCustomObject] @{ + text = $connectorDescriptionText; + } + } + if ($connectorCounter -eq 1) { + $baseCreateUiDefinition.parameters.steps += $baseDataConnectorStep + } + $currentStepNum = $baseCreateUiDefinition.parameters.steps.Count - 1 + $baseCreateUiDefinition.parameters.steps[$currentStepNum].elements += $baseDataConnectorTextElement + if ($connectorCounter -eq $contentToImport."Data Connectors".Count) { + $parserTextElement = [PSCustomObject] @{ + name = "dataconnectors-parser-text"; + type = "Microsoft.Common.TextBlock"; + options = [PSCustomObject] @{ + text = $parserText; + } + } + $connectDataSourcesLink = [PSCustomObject] @{ + name = "dataconnectors-link2"; + type = "Microsoft.Common.TextBlock"; + options = [PSCustomObject] @{ + link = [PSCustomObject] @{ + label = "Learn more about connecting data sources"; + uri = "https://docs.microsoft.com/azure/sentinel/connect-data-sources"; + } + } + } + if ($isParserAvailable) { + $baseCreateUiDefinition.parameters.steps[$currentStepNum].elements += $parserTextElement + } + $baseCreateUiDefinition.parameters.steps[$currentStepNum].elements += $connectDataSourcesLink + } + + # Update Connector Counter + $connectorCounter += 1 + } + elseif ($objectKeyLowercase -eq "savedsearches") { + $isStandardTemplate = $false + $searchData = $json # Assume input is basic array of SavedSearches to start + # Check if SavedSearch input file uses direct structure given by export + if ($searchData -isnot [System.Array] -and $searchData.value) { + $searchData = $searchData.value + } + # Check if SavedSearch input file uses standard template structure + if ($searchData -isnot [System.Array] -and $searchData.resources) { + $isStandardTemplate = $true + $searchData = $searchData.resources + } + if ($searchData -is [System.Array] -and !$isStandardTemplate) { + foreach ($search in $searchData) { + $savedSearchIdParameterName = "savedsearch$savedSearchCounter-id" + $savedSearchIdParameter = [PSCustomObject] @{ type = "string"; defaultValue = "[newGuid()]"; minLength = 1; metadata = [PSCustomObject] @{ description = "Unique id for the watchlist" }; } + $baseMainTemplate.parameters | Add-Member -MemberType NoteProperty -Name $savedSearchIdParameterName -Value $savedSearchIdParameter + + $savedSearchResource = [PSCustomObject]@{ + type = "Microsoft.OperationalInsights/workspaces/savedSearches"; + apiVersion = "2020-08-01"; + name = "[concat(parameters('workspace'),'/',parameters('$savedSearchIdParameterName'))]"; + properties = [PSCustomObject]@{ + category = $search.properties.category; + displayName = $search.properties.displayName; + query = $search.properties.query; + functionAlias = $search.properties.functionAlias; + version = $search.properties.version; + }; + } + $baseMainTemplate.resources += $savedSearchResource + $savedSearchCounter++ + } + } + elseif ($isStandardTemplate) { + $baseMainTemplate.resources += $searchData + } + } + elseif ($objectKeyLowercase -eq "watchlists") { + $watchlistData = $json.resources[0] + + $watchlistName = $watchlistData.properties.displayName; + if ($contentToImport.Metadata) { + $baseMainTemplate.variables | Add-Member -NotePropertyName $watchlistName -NotePropertyValue $watchlistName + $baseMainTemplate.variables | Add-Member -NotePropertyName "_$watchlistName" -NotePropertyValue "[variables('$watchlistName')]" + } + + $DependencyCriteria += [PSCustomObject]@{ + kind = "Watchlist"; + contentId = "[variables('_$watchlistName')]"; + version = $contentToImport.Version; + }; + + #Handle CreateUiDefinition Base Step + if ($watchlistCounter -eq 1) { + $baseWatchlistStep = [PSCustomObject]@{ + name = "watchlists"; + label = "Watchlists"; + subLabel = [PSCustomObject]@{ + preValidation = "Configure the watchlists"; + postValidation = "Done"; + } + bladeTitle = "Watchlists"; + elements = @( + [PSCustomObject]@{ + name = "watchlists-text"; + type = "Microsoft.Common.TextBlock"; + options = [PSCustomObject]@{ + text = "Microsoft Sentinel watchlists enable the collection of data from external data sources for correlation with the events in your Microsoft Sentinel environment. Once created, you can use watchlists in your search, detection rules, threat hunting, and response playbooks. Watchlists are stored in your Microsoft Sentinel workspace as name-value pairs and are cached for optimal query performance and low latency. Once deployment is successful, the installed watchlists will be available in the Watchlists blade under 'My Watchlists'."; + link = [PSCustomObject]@{ + label = "Learn more"; + uri = "https://aka.ms/sentinelwatchlists"; + } + } + } + ) + } + $baseCreateUiDefinition.parameters.steps += $baseWatchlistStep + } + + #Handle CreateUiDefinition Step Sub-Element + $watchlistDescriptionText = $(if ($contentToImport.WatchlistDescription -and $contentToImport.WatchlistDescription -is [System.Array]) { $contentToImport.WatchlistDescription[$watchlistCounter - 1] } elseif ($contentToImport.WatchlistDescription -and $contentToImport.WatchlistDescription -is [System.String]) { $contentToImport.WatchlistDescription } else { $watchlistData.properties.description }) + $currentStepNum = $baseCreateUiDefinition.parameters.steps.Count - 1 + $watchlistStepElement = [PSCustomObject]@{ + name = "watchlist$watchlistCounter"; + type = "Microsoft.Common.Section"; + label = $watchlistData.properties.displayName; + elements = @( + [PSCustomObject]@{ + name = "watchlist$watchlistCounter-text"; + type = "Microsoft.Common.TextBlock"; + options = [PSCustomObject]@{ + text = $watchlistDescriptionText + } + } + ) + } + $baseCreateUiDefinition.parameters.steps[$currentStepNum].elements += $watchlistStepElement + + # Add Watchlist ID to MainTemplate parameters + $watchlistIdParameterName = "watchlist$watchlistCounter-id" + $watchlistIdParameter = [PSCustomObject] @{ type = "string"; defaultValue = "[newGuid()]"; minLength = 1; metadata = [PSCustomObject] @{ description = "Unique id for the watchlist" }; } + $baseMainTemplate.parameters | Add-Member -MemberType NoteProperty -Name $watchlistIdParameterName -Value $watchlistIdParameter + + # Replace watchlist resource id + $watchlistData.name = "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',parameters('watchlist$watchlistCounter-id'))]" + + # Handle MainTemplate Resource + $baseMainTemplate.resources += $watchlistData #Assume 1 watchlist per template + + # Update Watchlist Counter + $watchlistCounter += 1 + } + } + else { + if ($file -match "(\.yaml)$") { + $objectKeyLowercase = $objectProperties.Name.ToLower() + if ($objectKeyLowercase -eq "hunting queries") { + Write-Host "Generating Hunting Query using $file" + $content = '' + foreach ($line in $rawData) { + $content = $content + "`n" + $line + } + try { + $yaml = ConvertFrom-YAML $content + } + catch { + Write-Host "Failed to deserialize $file" -ForegroundColor Red + break; + } + + $fileName = Split-Path $file -leafbase; + $fileName = $fileName + "_HuntingQueries"; + $baseMainTemplate.variables | Add-Member -NotePropertyName "huntingQueryVersion$huntingQueryCounter" -NotePropertyValue (($null -ne $yaml.version) ? "$($yaml.version)" : "1.0.0") + $baseMainTemplate.variables | Add-Member -NotePropertyName "huntingQuerycontentId$huntingQueryCounter" -NotePropertyValue $yaml.id + $baseMainTemplate.variables | Add-Member -NotePropertyName "_huntingQuerycontentId$huntingQueryCounter" -NotePropertyValue "[variables('huntingQuerycontentId$huntingQueryCounter')]" + $DependencyCriteria += [PSCustomObject]@{ + kind = "HuntingQuery"; + contentId = "[variables('_huntingQuerycontentId$huntingQueryCounter')]"; + version = "[variables('huntingQueryVersion$huntingQueryCounter')]"; + }; + + if ($huntingQueryCounter -eq 1) { + if (!$(queryResourceExists) -and !$contentToImport.TemplateSpec) { + $baseHuntingQueryResource = [PSCustomObject] @{ + type = "Microsoft.OperationalInsights/workspaces"; + apiVersion = "2021-06-01"; + name = "[parameters('workspace')]"; + location = "[parameters('workspace-location')]"; + resources = @() + } + $baseMainTemplate.resources += $baseHuntingQueryResource + } + if (!$contentToImport.TemplateSpec -and $null -eq $baseMainTemplate.variables.'workspace-dependency') { + #Add parser dependency variable once to ensure validation passes. + $baseMainTemplate.variables | Add-Member -MemberType NoteProperty -Name "workspace-dependency" -Value "[concat('Microsoft.OperationalInsights/workspaces/', parameters('workspace'))]" + } + $huntingQueryBaseStep = [PSCustomObject] @{ + name = "huntingqueries"; + label = "Hunting Queries"; + bladeTitle = "Hunting Queries"; + elements = @( + [PSCustomObject] @{ + name = "huntingqueries-text"; + type = "Microsoft.Common.TextBlock"; + options = [PSCustomObject] @{ + text = $contentToImport.HuntingQueryBladeDescription ? $contentToImport.HuntingQueryBladeDescription : "This solution installs the following hunting queries. After installing the solution, run these hunting queries to hunt for threats in Manage solution view. "; + } + }, + [PSCustomObject] @{ + name = "huntingqueries-link"; + type = "Microsoft.Common.TextBlock"; + options = [PSCustomObject] @{ + link = [PSCustomObject] @{ + label = "Learn more"; + uri = "https://docs.microsoft.com/azure/sentinel/hunting" + } + } + }) + } + $baseCreateUiDefinition.parameters.steps += $huntingQueryBaseStep + } + + $huntingQueryObj = [PSCustomObject] @{ + type = $contentToImport.TemplateSpec ? "Microsoft.OperationalInsights/savedSearches" : "savedSearches"; + apiVersion = "2020-08-01"; + name = $contentToImport.TemplateSpec ? "$($solutionName.Replace(' ', '_'))_Hunting_Query_$huntingQueryCounter" : "$solutionName Hunting Query $huntingQueryCounter"; + location = "[parameters('workspace-location')]"; + properties = [PSCustomObject] @{ + eTag = "*"; + displayName = $yaml.name; + category = "Hunting Queries"; + query = $yaml.query; + version = $contentToImport.TemplateSpec ? 2 : 1; + tags = @(); + } + } + + $huntingQueryDescription = "" + if ($yaml.description) { + $huntingQueryDescription = $yaml.description.substring(1, $yaml.description.length - 3) + $descriptionObj = [PSCustomObject]@{ + name = "description"; + value = $huntingQueryDescription + } + $huntingQueryObj.properties.tags += $descriptionObj + $huntingQueryDescription = "$huntingQueryDescription " + } + if ($yaml.tactics -and $yaml.tactics.Count -gt 0) { + $tacticsObj = [PSCustomObject]@{ + name = "tactics"; + value = $yaml.tactics -join "," + } + if ($tacticsObj.value.ToString() -match ' ') { + $tacticsObj.value = $tacticsObj.value -replace ' ', '' + } + $huntingQueryObj.properties.tags += $tacticsObj + } + + if ($yaml.relevantTechniques -and $yaml.relevantTechniques.Count -gt 0) { + $relevantTechniquesObj = [PSCustomObject]@{ + name = "techniques"; + value = $yaml.relevantTechniques -join "," + } + if ($relevantTechniquesObj.value.ToString() -match ' ') { + $relevantTechniquesObj.value = $relevantTechniquesObj.value -replace ' ', '' + } + $huntingQueryObj.properties.tags += $relevantTechniquesObj + } + + if($contentToImport.TemplateSpec) { + + $baseMainTemplate.variables | Add-Member -NotePropertyName "huntingQueryId$huntingQueryCounter" -NotePropertyValue "[resourceId('Microsoft.OperationalInsights/savedSearches', variables('_huntingQuerycontentId$huntingQueryCounter'))]" + $baseMainTemplate.variables | Add-Member -NotePropertyName "huntingQueryTemplateSpecName$huntingQueryCounter" -NotePropertyValue "[concat(parameters('workspace'),'-hq-',uniquestring(variables('_huntingQuerycontentId$huntingQueryCounter')))]" + if (!$baseMainTemplate.variables.workspaceResourceId) { + $baseMainTemplate.variables | Add-Member -NotePropertyName "workspaceResourceId" -NotePropertyValue "[resourceId('microsoft.OperationalInsights/Workspaces', parameters('workspace'))]" + } + + $baseHuntingQueryTemplateSpec = [PSCustomObject]@{ + type = "Microsoft.Resources/templateSpecs"; + apiVersion = "2021-05-01"; + name = "[variables('huntingQueryTemplateSpecName$huntingQueryCounter')]"; + location = "[parameters('workspace-location')]"; + tags = [PSCustomObject]@{ + "hidden-sentinelWorkspaceId" = "[variables('workspaceResourceId')]"; + "hidden-sentinelContentType" = "HuntingQuery"; + }; + properties = [PSCustomObject]@{ + description = "$($solutionName) Hunting Query $huntingQueryCounter with template"; + displayName = "$($solutionName) Hunting Query template"; + } + } + + $baseMainTemplate.resources += $baseHuntingQueryTemplateSpec + $author = $contentToImport.Author.Split(" - "); + $authorDetails = [PSCustomObject]@{ + name = $author[0]; + }; + if($null -ne $author[1]) + { + $authorDetails | Add-Member -NotePropertyName "email" -NotePropertyValue "[variables('_email')]" + } + + $huntingQueryMetadata = [PSCustomObject]@{ + type = "Microsoft.OperationalInsights/workspaces/providers/metadata"; + apiVersion = "2022-01-01-preview"; + name = "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('HuntingQuery-', last(split(variables('huntingQueryId$huntingQueryCounter'),'/'))))]"; + properties = [PSCustomObject]@{ + description = "$($solutionName) Hunting Query $huntingQueryCounter"; + parentId = "[variables('huntingQueryId$huntingQueryCounter')]"; + contentId = "[variables('_huntingQuerycontentId$huntingQueryCounter')]"; + kind = "HuntingQuery"; + version = "[variables('huntingQueryVersion$huntingQueryCounter')]"; + source = [PSCustomObject]@{ + kind = "Solution"; + name = $contentToImport.Name; + sourceId = "[variables('_solutionId')]" + }; + author = $authorDetails; + support = $baseMetadata.support + } + } + + # Add templateSpecs/versions resource to hold actual content + $huntingQueryTemplateSpecContent = [PSCustomObject]@{ + type = "Microsoft.Resources/templateSpecs/versions"; + apiVersion = "2021-05-01"; + name = "[concat(variables('huntingQueryTemplateSpecName$huntingQueryCounter'),'/',variables('huntingQueryVersion$huntingQueryCounter'))]"; + location = "[parameters('workspace-location')]"; + tags = [PSCustomObject]@{ + "hidden-sentinelWorkspaceId" = "[variables('workspaceResourceId')]"; + "hidden-sentinelContentType" = "HuntingQuery"; + }; + dependsOn = @( + "[resourceId('Microsoft.Resources/templateSpecs', variables('huntingQueryTemplateSpecName$huntingQueryCounter'))]" + ); + properties = [PSCustomObject]@{ + description = "$($fileName) Hunting Query with template version $($contentToImport.Version)"; + mainTemplate = [PSCustomObject]@{ + '$schema' = "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#"; + contentVersion = "[variables('huntingQueryVersion$huntingQueryCounter')]"; + parameters = [PSCustomObject]@{}; + variables = [PSCustomObject]@{}; + resources = @( + # workbook + $huntingQueryObj, + # Metadata + $huntingQueryMetadata + ) + } + } + } + $baseMainTemplate.resources += $huntingQueryTemplateSpecContent + } + else{ + if(!$contentToImport.TemplateSpec) + { + $dependsOn = @( + "[variables('workspace-dependency')]" + ); + + $huntingQueryObj | Add-Member -NotePropertyName "dependsOn" -NotePropertyValue $dependsOn + } + $baseMainTemplate.resources[$(getQueryResourceLocation)].resources += $huntingQueryObj + } + $dependencyDescription = "" + if ($yaml.requiredDataConnectors) { + $dependencyDescription = "It depends on the $($yaml.requiredDataConnectors.connectorId) data connector and $($($yaml.requiredDataConnectors.dataTypes)) data type and $($yaml.requiredDataConnectors.connectorId) parser." + } + $huntingQueryElement = [PSCustomObject]@{ + name = "huntingquery$huntingQueryCounter"; + type = "Microsoft.Common.Section"; + label = $yaml.name; + elements = @() + } + $huntingQueryElementDescription = [PSCustomObject]@{ + name = "huntingquery$huntingQueryCounter-text"; + type = "Microsoft.Common.TextBlock"; + options = [PSCustomObject]@{ + text = "$($huntingQueryDescription)$dependencyDescription"; + } + } + if ($huntingQueryDescription -or $dependencyDescription) { + $huntingQueryElement.elements += $huntingQueryElementDescription + } + $baseCreateUiDefinition.parameters.steps[$baseCreateUiDefinition.parameters.steps.Count - 1].elements += $huntingQueryElement + + # Update HuntingQuery Counter + $huntingQueryCounter += 1 + } + else { + # If yaml and not hunting query, process as Alert Rule + Write-Host "Generating Alert Rule using $file" + if ($analyticRuleCounter -eq 1) { + $baseAnalyticRuleStep = [PSCustomObject] @{ + name = "analytics"; + label = "Analytics"; + subLabel = [PSCustomObject] @{ + preValidation = "Configure the analytics"; + postValidation = "Done"; + }; + bladeTitle = "Analytics"; + elements = @( + [PSCustomObject] @{ + name = "analytics-text"; + type = "Microsoft.Common.TextBlock"; + options = [PSCustomObject] @{ + text = $contentToImport.AnalyticalRuleBladeDescription ? $contentToImport.AnalyticalRuleBladeDescription : "This solution installs the following analytic rule templates. After installing the solution, create and enable analytic rules in Manage solution view."; + } + }, + [PSCustomObject] @{ + name = "analytics-link"; + type = "Microsoft.Common.TextBlock"; + options = [PSCustomObject] @{ + link = [PSCustomObject] @{ + label = "Learn more"; + uri = "https://docs.microsoft.com/azure/sentinel/tutorial-detect-threats-custom?WT.mc_id=Portal-Microsoft_Azure_CreateUIDef"; + } + } + } + ) + } + $baseCreateUiDefinition.parameters.steps += $baseAnalyticRuleStep + } + $yamlPropertiesToCopyFrom = "name", "severity", "triggerThreshold", "query" + $yamlPropertiesToCopyTo = "displayName", "severity", "triggerThreshold", "query" + $alertRuleParameterName = "analytic$analyticRuleCounter-id" + $alertRule = [PSCustomObject] @{ description = ""; displayName = ""; enabled = $false; query = ""; queryFrequency = ""; queryPeriod = ""; severity = ""; suppressionDuration = ""; suppressionEnabled = $false; triggerOperator = ""; triggerThreshold = 0; } + $alertRuleParameter = [PSCustomObject] @{ type = "string"; defaultValue = "[newGuid()]"; minLength = 1; metadata = [PSCustomObject] @{ description = "Unique id for the scheduled alert rule" }; } + $content = '' + + $fileName = Split-Path $file -leafbase; + $fileName = $fileName + "_AnalyticalRules"; + foreach ($line in $rawData) { + $content = $content + "`n" + $line + } + try { + $yaml = ConvertFrom-YAML $content # Convert YAML to PSObject + } + catch { + Write-Host "Failed to deserialize $file" -ForegroundColor Red + break; + } + $baseMainTemplate.variables | Add-Member -NotePropertyName "analyticRuleVersion$analyticRuleCounter" -NotePropertyValue (($null -ne $yaml.version) ? "$($yaml.version)" : "1.0.0") + $baseMainTemplate.variables | Add-Member -NotePropertyName "analyticRulecontentId$analyticRuleCounter" -NotePropertyValue "$($yaml.id)" + $baseMainTemplate.variables | Add-Member -NotePropertyName "_analyticRulecontentId$analyticRuleCounter" -NotePropertyValue "[variables('analyticRulecontentId$analyticRuleCounter')]" + $DependencyCriteria += [PSCustomObject]@{ + kind = "AnalyticsRule"; + contentId = "[variables('analyticRulecontentId$analyticRuleCounter')]"; + #post bug bash ,remove this below comments! + version = "[variables('analyticRuleVersion$analyticRuleCounter')]"; + }; + # Copy all directly transposable properties + foreach ($yamlProperty in $yamlPropertiesToCopyFrom) { + $index = $yamlPropertiesToCopyFrom.IndexOf($yamlProperty) + $alertRule.$($yamlPropertiesToCopyTo[$index]) = $yaml.$yamlProperty + } + + if($contentToImport.TemplateSpec) + { + $alertRule | Add-Member -NotePropertyName status -NotePropertyValue ($yaml.status ? $yaml.status : "Available") # Add requiredDataConnectors property if exists + } + + if($yaml.requiredDataConnectors) + { + $alertRule | Add-Member -NotePropertyName requiredDataConnectors -NotePropertyValue $yaml.requiredDataConnectors # Add requiredDataConnectors property if exists + for($i=0; $i -lt $yaml.requiredDataConnectors.connectorId.count; $i++) + { + $alertRule.requiredDataConnectors[$i].connectorId = ($yaml.requiredDataConnectors[$i].connectorId.GetType().Name -is [object]) ? ($yaml.requiredDataConnectors[$i].connectorId -join ',') : $yaml.requiredDataConnectors[$i].connectorId; + } + } + + if (!$yaml.severity) { + $alertRule.severity = "Medium" + } + + # Content Modifications + $triggerOperators = [PSCustomObject] @{ gt = "GreaterThan" ; lt = "LessThan" ; eq = "Equal" ; ne = "NotEqual" } + $alertRule.triggerOperator = $triggerOperators.$($yaml.triggerOperator) + if ($yaml.tactics -and ($yaml.tactics.Count -gt 0) ) { + if ($yaml.tactics -match ' ') { + $yaml.tactics = $yaml.tactics -replace ' ', '' + } + $alertRule | Add-Member -NotePropertyName tactics -NotePropertyValue $yaml.tactics # Add Tactics property if exists + } + $alertRule.description = $yaml.description.TrimEnd() #remove newlines at the end of the string if there are any. + if ($alertRule.description.StartsWith("'") -or $alertRule.description.StartsWith('"')) { + # Remove surrounding single-quotes (') from YAML block literal string, in case the string starts with a single quote in Yaml. + # This block is for backwards compatibility as YAML doesn't require having strings quotes by single (or double) quotes + $alertRule.description = $alertRule.description.substring(1, $alertRule.description.length - 2) + } + + # Check whether Day or Hour/Minut format need be used + function checkISO8601Format($field) { + if ($field.IndexOf("D") -ne -1) { + return "P$field" + } + else { + "PT$field" + } + } + + if($yaml.kind.ToUpper() -eq "Scheduled") + { + $alertRule.queryFrequency = $(checkISO8601Format $yaml.queryFrequency.ToUpper()) + $alertRule.queryPeriod = $(checkISO8601Format $yaml.queryPeriod.ToUpper()) + } + else { + $alertRule.PSObject.Properties.Remove('queryFrequency'); + $alertRule.PSObject.Properties.Remove('queryPeriod'); + $alertRule.PSObject.Properties.Remove('triggerOperator'); + $alertRule.PSObject.Properties.Remove('triggerThreshold'); + } + $alertRule.suppressionDuration = "PT1H" + + # Handle optional fields + foreach ($yamlField in @("entityMappings", "eventGroupingSettings", "customDetails", "alertDetailsOverride")) { + if ($yaml.$yamlField) { + $alertRule | Add-Member -MemberType NoteProperty -Name $yamlField -Value $yaml.$yamlField + } + } + + # Create Alert Rule Resource Object + $newAnalyticRule = [PSCustomObject]@{ + type = $contentToImport.TemplateSpec ? "Microsoft.SecurityInsights/AlertRuleTemplates" : "Microsoft.OperationalInsights/workspaces/providers/alertRules"; + name = "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',parameters('analytic$analyticRuleCounter-id'))]"; + apiVersion = "2022-04-01-preview"; + kind = "$($yaml.kind)"; + location = "[parameters('workspace-location')]"; + properties = $alertRule; + } + + if($contentToImport.TemplateSpec) { + + $baseMainTemplate.variables | Add-Member -NotePropertyName "analyticRuleId$analyticRuleCounter" -NotePropertyValue "[resourceId('Microsoft.SecurityInsights/AlertRuleTemplates', variables('analyticRulecontentId$analyticRuleCounter'))]" + $baseMainTemplate.variables | Add-Member -NotePropertyName "analyticRuleTemplateSpecName$analyticRuleCounter" -NotePropertyValue "[concat(parameters('workspace'),'-ar-',uniquestring(variables('_analyticRulecontentId$analyticRuleCounter')))]" + if (!$baseMainTemplate.variables.workspaceResourceId) { + $baseMainTemplate.variables | Add-Member -NotePropertyName "workspaceResourceId" -NotePropertyValue "[resourceId('microsoft.OperationalInsights/Workspaces', parameters('workspace'))]" + } + + $baseAnalyticRuleTemplateSpec = [PSCustomObject]@{ + type = "Microsoft.Resources/templateSpecs"; + apiVersion = "2021-05-01"; + name = "[variables('analyticRuleTemplateSpecName$analyticRuleCounter')]"; + location = "[parameters('workspace-location')]"; + tags = [PSCustomObject]@{ + "hidden-sentinelWorkspaceId" = "[variables('workspaceResourceId')]"; + "hidden-sentinelContentType" = "AnalyticsRule"; + }; + properties = [PSCustomObject]@{ + description = "$($solutionName) Analytics Rule $analyticRuleCounter with template"; + displayName = "$($solutionName) Analytics Rule template"; + } + } + + $newAnalyticRule.name = "[variables('AnalyticRulecontentId$analyticRuleCounter')]" + $baseMainTemplate.resources += $baseAnalyticRuleTemplateSpec + $author = $contentToImport.Author.Split(" - "); + $authorDetails = [PSCustomObject]@{ + name = $author[0]; + }; + if($null -ne $author[1]) + { + $authorDetails | Add-Member -NotePropertyName "email" -NotePropertyValue "[variables('_email')]" + } + + $analyticRuleMetadata = [PSCustomObject]@{ + type = "Microsoft.OperationalInsights/workspaces/providers/metadata"; + apiVersion = "2022-01-01-preview"; + name = "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('AnalyticsRule-', last(split(variables('analyticRuleId$analyticRuleCounter'),'/'))))]"; + properties = [PSCustomObject]@{ + description = "$($solutionName) Analytics Rule $analyticRuleCounter"; + parentId = "[variables('analyticRuleId$analyticRuleCounter')]"; + contentId = "[variables('_analyticRulecontentId$analyticRuleCounter')]"; + kind = "AnalyticsRule"; + # Need to remove the below assigned property for the yaml version after bug bash + version = "[variables('analyticRuleVersion$analyticRuleCounter')]"; + source = [PSCustomObject]@{ + kind = "Solution"; + name = $contentToImport.Name; + sourceId = "[variables('_solutionId')]" + }; + author = $authorDetails; + support = $baseMetadata.support + } + } + + # Add templateSpecs/versions resource to hold actual content + $analyticRuleTemplateSpecContent = [PSCustomObject]@{ + type = "Microsoft.Resources/templateSpecs/versions"; + apiVersion = "2021-05-01"; + name = "[concat(variables('analyticRuleTemplateSpecName$analyticRuleCounter'),'/',variables('analyticRuleVersion$analyticRuleCounter'))]"; + location = "[parameters('workspace-location')]"; + tags = [PSCustomObject]@{ + "hidden-sentinelWorkspaceId" = "[variables('workspaceResourceId')]"; + "hidden-sentinelContentType" = "AnalyticsRule"; + }; + dependsOn = @( + "[resourceId('Microsoft.Resources/templateSpecs', variables('analyticRuleTemplateSpecName$analyticRuleCounter'))]" + ); + properties = [PSCustomObject]@{ + description = "$($fileName) Analytics Rule with template version $($contentToImport.Version)"; + mainTemplate = [PSCustomObject]@{ + '$schema' = "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#"; + contentVersion = "[variables('analyticRuleVersion$analyticRuleCounter')]"; + parameters = [PSCustomObject]@{}; + variables = [PSCustomObject]@{}; + resources = @( + # Analytics Rule + $newAnalyticRule, + # Metadata + $analyticRuleMetadata + ) + } + } + } + $baseMainTemplate.resources += $analyticRuleTemplateSpecContent + } + else { + # Add Resource and Parameters to Template + $baseMainTemplate.resources += $newAnalyticRule + } + + if(!$contentToImport.TemplateSpec) + { + $baseMainTemplate.parameters | Add-Member -MemberType NoteProperty -Name $alertRuleParameterName -Value $alertRuleParameter + } + $alertRuleUIParameter = [PSCustomObject] @{ name = "analytic$analyticRuleCounter"; type = "Microsoft.Common.Section"; label = $alertRule.displayName; elements = @( [PSCustomObject] @{ name = "analytic$analyticRuleCounter-text"; type = "Microsoft.Common.TextBlock"; options = @{ text = $alertRule.description; } } ) } + $baseCreateUiDefinition.parameters.steps[$baseCreateUiDefinition.parameters.steps.Count - 1].elements += $alertRuleUIParameter + + # Update Counter + $analyticRuleCounter += 1 + } + } + else { + # Assume file is Parser due to parsers having inconsistent types. (.txt, .kql, or none) + Write-Host "Generating Data Parser using $file" + if ($parserCounter -eq 1 -and $null -eq $baseMainTemplate.variables.'workspace-dependency' -and !$contentToImport.TemplateSpec) { + # Add parser dependency variable once to ensure validation passes. + $baseMainTemplate.variables | Add-Member -MemberType NoteProperty -Name "workspace-dependency" -Value "[concat('Microsoft.OperationalInsights/workspaces/', parameters('workspace'))]" + } + + $fileName = Split-Path $file -leafbase; + + function getFileNameFromPath ($inputFilePath) { + # Split out path + $output = $inputFilePath.Split("/") + $output = $output[$output.Length - 1] + + # Split out file type + $output = $output.Split(".")[0] + return $output + } + $content = '' + $rawData = $rawData.Split("`n") + foreach ($line in $rawData) { + # Remove comment lines before condensing query + if (!$line.StartsWith("//")) { + $content = $content + "`n" + $line + } + } + + # Use File Name as Parser Name + $functionAlias = getFileNameFromPath $file + $baseMainTemplate.variables | Add-Member -NotePropertyName "parserVersion$parserCounter" -NotePropertyValue "1.0.0" + $baseMainTemplate.variables | Add-Member -NotePropertyName "parserContentId$parserCounter" -NotePropertyValue "$($functionAlias)-Parser" + $baseMainTemplate.variables | Add-Member -NotePropertyName "_parserContentId$parserCounter" -NotePropertyValue "[variables('parserContentId$parserCounter')]" + $DependencyCriteria += [PSCustomObject]@{ + kind = "Parser"; + contentId = "[variables('_parserContentId$parserCounter')]"; + version = "[variables('parserVersion$parserCounter')]"; + }; + + if($contentToImport.TemplateSpec) { + $baseMainTemplate.variables | Add-Member -NotePropertyName "parserName$parserCounter" -NotePropertyValue "$fileName" + $baseMainTemplate.variables | Add-Member -NotePropertyName "_parserName$parserCounter" -NotePropertyValue "[concat(parameters('workspace'),'/',variables('parserName$parserCounter'))]" + $baseMainTemplate.variables | Add-Member -NotePropertyName "parserId$parserCounter" -NotePropertyValue "[resourceId('Microsoft.OperationalInsights/workspaces/savedSearches', parameters('workspace'), variables('parserName$parserCounter'))]" + $baseMainTemplate.variables | Add-Member -NotePropertyName "_parserId$parserCounter" -NotePropertyValue "[variables('parserId$parserCounter')]" + $baseMainTemplate.variables | Add-Member -NotePropertyName "parserTemplateSpecName$parserCounter" -NotePropertyValue "[concat(parameters('workspace'),'-pr-',uniquestring(variables('_parserContentId$parserCounter')))]" + # Add workspace resource ID if not available + if (!$baseMainTemplate.variables.workspaceResourceId) { + $baseMainTemplate.variables | Add-Member -NotePropertyName "workspaceResourceId" -NotePropertyValue "[resourceId('microsoft.OperationalInsights/Workspaces', parameters('workspace'))]" + } + # Add base templateSpec + $baseParserTemplateSpec = [PSCustomObject]@{ + type = "Microsoft.Resources/templateSpecs"; + apiVersion = "2021-05-01"; + name = "[variables('parserTemplateSpecName$parserCounter')]"; + location = "[parameters('workspace-location')]"; + tags = [PSCustomObject]@{ + "hidden-sentinelWorkspaceId" = "[variables('workspaceResourceId')]"; + "hidden-sentinelContentType" = "Parser"; + }; + properties = [PSCustomObject]@{ + description = "$($fileName) Data Parser with template"; + displayName = "$($fileName) Data Parser template"; + } + } + $baseMainTemplate.resources += $baseParserTemplateSpec + + # Parser Content + $parserContent = [PSCustomObject]@{ + name = "[variables('_parserName$parserCounter')]"; + apiVersion = "2020-08-01"; + type = "Microsoft.OperationalInsights/workspaces/savedSearches"; + location = "[parameters('workspace-location')]"; + properties = [PSCustomObject]@{ + eTag = "*" + displayName = "$fileName" + category = "Samples" + functionAlias = "$functionAlias" + query = "$content" + version = 1 + tags = @([PSCustomObject]@{ + "name" = "description" + "value" = "$($fileName)" + }; + ) + } + } + + $author = $contentToImport.Author.Split(" - "); + $authorDetails = [PSCustomObject]@{ + name = $author[0]; + }; + if($null -ne $author[1]) + { + $authorDetails | Add-Member -NotePropertyName "email" -NotePropertyValue "[variables('_email')]" + } + $parserMetadata = [PSCustomObject]@{ + type = "Microsoft.OperationalInsights/workspaces/providers/metadata"; + apiVersion = "2022-01-01-preview"; + name = "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('Parser-', last(split(variables('_parserId$parserCounter'),'/'))))]"; + dependsOn = @( + "[variables('_parserName$parserCounter')]" + ); + properties = [PSCustomObject]@{ + parentId = "[resourceId('Microsoft.OperationalInsights/workspaces/savedSearches', parameters('workspace'), variables('parserName$parserCounter'))]" + contentId = "[variables('_parserContentId$parserCounter')]"; + kind = "Parser"; + version = "[variables('parserVersion$parserCounter')]"; + source = [PSCustomObject]@{ + name = $contentToImport.Name; + kind = "Solution"; + sourceId = "[variables('_solutionId')]" + }; + author = $authorDetails; + support = $baseMetadata.support + } + } + + # Add templateSpecs/versions resource to hold actual content + $parserTemplateSpecContent = [PSCustomObject]@{ + type = "Microsoft.Resources/templateSpecs/versions"; + apiVersion = "2021-05-01"; + name = "[concat(variables('parserTemplateSpecName$parserCounter'),'/',variables('parserVersion$parserCounter'))]"; + location = "[parameters('workspace-location')]"; + tags = [PSCustomObject]@{ + "hidden-sentinelWorkspaceId" = "[variables('workspaceResourceId')]"; + "hidden-sentinelContentType" = "Parser"; + }; + dependsOn = @( + "[resourceId('Microsoft.Resources/templateSpecs', variables('parserTemplateSpecName$parserCounter'))]" + ); + properties = [PSCustomObject]@{ + description = "$($fileName) Data Parser with template version $($contentToImport.Version)"; + mainTemplate = [PSCustomObject]@{ + '$schema' = "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#"; + contentVersion = "[variables('parserVersion$parserCounter')]"; + parameters = [PSCustomObject]@{}; + variables = [PSCustomObject]@{}; + resources = @( + # Parser + $parserContent, + # Metadata + $parserMetadata + ) + } + } + } + $baseMainTemplate.resources += $parserTemplateSpecContent + + $parserObj = [PSCustomObject] @{ + type = "Microsoft.OperationalInsights/workspaces/savedSearches"; + apiVersion = "2021-06-01"; + name = "[variables('_parserName$parserCounter')]"; + location = "[parameters('workspace-location')]"; + properties = [PSCustomObject] @{ + eTag = "*"; + displayName = "$fileName"; + category = "Samples"; + functionAlias = "$functionAlias"; + query = $content; + version = 1; + } + } + $baseMainTemplate.resources += $parserObj + + $parserMetadata = [PSCustomObject]@{ + type = "Microsoft.OperationalInsights/workspaces/providers/metadata"; + apiVersion = "2022-01-01-preview"; + location = "[parameters('workspace-location')]"; + name = "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('Parser-', last(split(variables('_parserId$parserCounter'),'/'))))]"; + dependsOn = @( + "[variables('_parserId$parserCounter')]" + ); + properties = [PSCustomObject]@{ + parentId = "[resourceId('Microsoft.OperationalInsights/workspaces/savedSearches', parameters('workspace'), variables('parserName$parserCounter'))]" + contentId = "[variables('_parserContentId$parserCounter')]"; + kind = "Parser"; + version = "[variables('parserVersion$parserCounter')]"; + source = [PSCustomObject]@{ + kind = "Solution"; + name = $contentToImport.Name; + sourceId = "[variables('_solutionId')]" + }; + author = $authorDetails; + support = $baseMetadata.support + } + } + + $baseMainTemplate.resources += $parserMetadata + } + else { + if ($parserCounter -eq 1 -and $(queryResourceExists) -and !$contentToImport.TemplateSpec) { + $baseParserResource = [PSCustomObject] @{ + type = "Microsoft.OperationalInsights/workspaces"; + apiVersion = "2020-08-01"; + name = "[parameters('workspace')]"; + location = "[parameters('workspace-location')]"; + resources = @( + + ) + } + $baseMainTemplate.resources += $baseParserResource + } + $parserObj = [PSCustomObject] @{ + type = "savedSearches"; + apiVersion = "2020-08-01"; + name = "$solutionName Data Parser"; + dependsOn = @( + "[variables('workspace-dependency')]" + ); + properties = [PSCustomObject] @{ + eTag = "*"; + displayName = "$solutionName Data Parser"; + category = "Samples"; + functionAlias = "$functionAlias"; + query = $content; + version = 1; + } + } + $baseMainTemplate.resources[$(getQueryResourceLocation)].resources += $parserObj + } + # Update Parser Counter + $parserCounter += 1 + } + } + } + } + elseif ($objectProperties.Name.ToLower() -eq "metadata") { + $finalPath = $metadataPath + $rawData = $null + try { + Write-Host "Downloading $finalPath" + $rawData = (New-Object System.Net.WebClient).DownloadString($finalPath) + } + catch { + Write-Host "Failed to download $finalPath -- Please ensure that it exists in $([System.Uri]::EscapeUriString($basePath))" -ForegroundColor Red + break; + } + + try { + $json = ConvertFrom-Json $rawData -ErrorAction Stop; # Determine whether content is JSON or YAML + $validJson = $true; + } + catch { + $validJson = $false; + } + + if ($validJson -and $json) { + # Create Metadata Resource Object + if ($json.support) { + $support = $json.support; + } + if ($json.categories) { + $categories = $json.categories; + } + + + $Author = $contentToImport.Author.Split(" - "); + + $newMetadata = [PSCustomObject]@{ + type = "Microsoft.OperationalInsights/workspaces/providers/metadata"; + apiVersion = "2022-01-01-preview"; + location = "[parameters('workspace-location')]"; + properties = [PSCustomObject] @{ + version = $contentToImport.Version; + kind = "Solution"; + }; + }; + + if($contentToImport.TemplateSpec) + { + $newMetadata.Properties | Add-Member -Name 'contentSchemaVersion' -Type NoteProperty -Value "2.0.0"; + } + + $source = [PSCustomObject]@{ + kind = "Solution"; + name = "$solutionName"; + }; + $authorDetails = [PSCustomObject]@{ + name = $Author[0]; + }; + if($null -ne $Author[1]) + { + $authorDetails | Add-Member -NotePropertyName "email" -NotePropertyValue "[variables('_email')]" + } + if ($solutionId) { + $newMetadata | Add-Member -Name 'name' -Type NoteProperty -Value "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/', variables('_solutionId'))]"; + $newMetadata.Properties | Add-Member -Name 'contentId' -Type NoteProperty -Value "[variables('_solutionId')]"; + $newMetadata.Properties | Add-Member -Name 'parentId' -Type NoteProperty -Value "[variables('_solutionId')]"; + + $source | Add-Member -Name 'sourceId' -Type NoteProperty -value "[variables('_solutionId')]"; + $newMetadata.Properties | Add-Member -Name 'source' -Type NoteProperty -value $source; + } + + $newMetadata.Properties | Add-Member -Name 'author' -Type NoteProperty -value $authorDetails + + $supportDetails = New-Object psobject; + + if ($support -and $support.psobject.properties["name"] -and $support.psobject.properties["name"].value) { + $supportDetails | Add-Member -Name 'name' -Type NoteProperty -value $support.psobject.properties["name"].value; + } + + if ($support -and $support.psobject.properties["email"] -and $support.psobject.properties["email"].value) { + $supportDetails | Add-Member -Name 'email' -Type NoteProperty -value $support.psobject.properties["email"].value; + } + + if ($support -and $support.psobject.properties["tier"] -and $support.psobject.properties["tier"].value) { + $supportDetails | Add-Member -Name 'tier' -Type NoteProperty -value $support.psobject.properties["tier"].value; + } + + if ($support -and $support.psobject.properties["link"] -and $support.psobject.properties["link"].value) { + $supportDetails | Add-Member -Name 'link' -Type NoteProperty -value $support.psobject.properties["link"].value; + } + + if ($support.psobject.properties["name"] -or $support.psobject.properties["email"] -or $support.psobject.properties["tier"] -or $support.psobject.properties["link"]) { + $newMetadata.Properties | Add-Member -Name 'support' -Type NoteProperty -value $supportDetails; + } + + $dependencies = [PSCustomObject]@{ + operator = "AND"; + criteria = $DependencyCriteria; + }; + + $newMetadata.properties | Add-Member -Name 'dependencies' -Type NoteProperty -Value $dependencies; + + if ($json.firstPublishDate -and $json.firstPublishDate -ne "") { + $newMetadata.Properties | Add-Member -Name 'firstPublishDate' -Type NoteProperty -value $json.firstPublishDate; + } + + if ($json.lastPublishDate -and $json.lastPublishDate -ne "") { + $newMetadata.Properties | Add-Member -Name 'lastPublishDate' -Type NoteProperty -value $json.lastPublishDate; + } + + if ($json.providers -and $json.providers -ne "") { + $newMetadata.Properties | Add-Member -Name 'providers' -Type NoteProperty -value $json.providers; + } + $categoriesDetails = New-Object psobject; + if ($categories -and $categories.psobject.properties['domains'] -and $categories.psobject.properties["domains"].Value.Length -gt 0) { + $categoriesDetails | Add-Member -Name 'domains' -Type NoteProperty -Value $categories.psobject.properties["domains"].Value; + $newMetadata.properties | Add-Member -Name 'categories' -Type NoteProperty -Value $categoriesDetails; + } + + if ($categories -and $categories.psobject.properties['verticals'] -and $categories.psobject.properties["verticals"].Value.Length -gt 0) { + $categoriesDetails | Add-Member -Name 'verticals' -Type NoteProperty -Value $categories.psobject.properties["verticals"].value; + $newMetadata.properties | Add-Member -Name 'categories' -Type NoteProperty -Value $categoriesDetails; + } + $baseMainTemplate.resources += $newMetadata; + } + else { + Write-Host "Failed to load Metadata file $file -- Please ensure that it exists in $([System.Uri]::EscapeUriString($basePath))" -ForegroundColor Red + } + } + } + + + # Update CreateUiDefinition Description with Content Counts + function updateDescriptionCount($counter, $emplaceString, $replaceString, $countStringCondition) { + if ($counter -gt 0) { + $ruleCountSubstring = "$emplaceString$counter" + $ruleCountString = $(if ($countStringCondition) { "$ruleCountSubstring, " } else { $ruleCountSubstring }) + $baseCreateUiDefinition.parameters.config.basics.description = $baseCreateUiDefinition.parameters.config.basics.description -replace $replaceString, $ruleCountString + } + else { + $baseCreateUiDefinition.parameters.config.basics.description = $baseCreateUiDefinition.parameters.config.basics.description -replace $replaceString, "" + } + } + function checkResourceCounts ($countList) { + if ($countList -isnot [System.Array]) { return $false } + else { + foreach ($count in $countList) { if ($count -gt 0) { return $true } } + return $false + } + } + if ($contentToImport.Description) { + $baseCreateUiDefinition.parameters.config.basics.description = $baseCreateUiDefinition.parameters.config.basics.description -replace "{{SolutionDescription}}", $contentToImport.Description + } + else { + $baseCreateUiDefinition.parameters.config.basics.description = $baseCreateUiDefinition.parameters.config.basics.description -replace "{{SolutionDescription}}", "" + } + + $analyticRuleCounter -= 1 + $workbookCounter -= 1 + $playbookCounter -= 1 + $connectorCounter -= 1 + $parserCounter -= 1 + $huntingQueryCounter -= 1 + $watchlistCounter -= 1 + updateDescriptionCount $connectorCounter "**Data Connectors:** " "{{DataConnectorCount}}" $(checkResourceCounts $parserCounter, $analyticRuleCounter, $workbookCounter, $playbookCounter, $huntingQueryCounter, $watchlistCounter) + updateDescriptionCount $parserCounter "**Parsers:** " "{{ParserCount}}" $(checkResourceCounts $analyticRuleCounter, $workbookCounter, $playbookCounter, $huntingQueryCounter, $watchlistCounter) + updateDescriptionCount $workbookCounter "**Workbooks:** " "{{WorkbookCount}}" $(checkResourceCounts $analyticRuleCounter, $playbookCounter, $huntingQueryCounter, $watchlistCounter) + updateDescriptionCount $analyticRuleCounter "**Analytic Rules:** " "{{AnalyticRuleCount}}" $(checkResourceCounts $playbookCounter, $huntingQueryCounter, $watchlistCounter) + updateDescriptionCount $huntingQueryCounter "**Hunting Queries:** " "{{HuntingQueryCount}}" $(checkResourceCounts $playbookCounter, $watchlistCounter) + updateDescriptionCount $watchlistCounter "**Watchlists:** " "{{WatchlistCount}}" $(checkResourceCounts @($playbookCounter)) + updateDescriptionCount $customConnectorsList.Count "**Custom Azure Logic Apps Connectors:** " "{{LogicAppCustomConnectorCount}}" $(checkResourceCounts @($playbookCounter)) + updateDescriptionCount ($playbookCounter - $customConnectorsList.Count) "**Playbooks:** " "{{PlaybookCount}}" $false + + # Update Logo in CreateUiDefinition Description + if ($contentToImport.Logo) { + $baseCreateUiDefinition.parameters.config.basics.description = $baseCreateUiDefinition.parameters.config.basics.description -replace "{{Logo}}", $contentToImport.Logo + } + else { + $baseCreateUiDefinition.parameters.config.basics.description = $baseCreateUiDefinition.parameters.config.basics.description -replace "{{Logo}}\n\n", "" + } + + # Update Metadata in MainTemplate + $baseMainTemplate.metadata.author = $(if ($contentToImport.Author) { $contentToImport.Author } else { "" }) + $baseMainTemplate.metadata.comments = $baseMainTemplate.metadata.comments -replace "{{SolutionName}}", $solutionName + + $repoRoot = $(git rev-parse --show-toplevel) + $solutionFolderName = $solutionName + $solutionFolder = "$repoRoot/Solutions/$solutionFolderName" + if (!(Test-Path -Path $solutionFolder)) { + New-Item -ItemType Directory $solutionFolder + } + $solutionFolder = "$solutionFolder/Package" + if (!(Test-Path -Path $solutionFolder)) { + New-Item -ItemType Directory $solutionFolder + } + $mainTemplateOutputPath = "$solutionFolder/mainTemplate.json" + $createUiDefinitionOutputPath = "$solutionFolder/createUiDefinition.json" + + try { + $baseMainTemplate | ConvertTo-Json -Depth $jsonConversionDepth | Out-File $mainTemplateOutputPath -Encoding utf8 + } + catch { + Write-Host "Failed to write output file $mainTemplateOutputPath" -ForegroundColor Red + break; + } + try { + # Sort UI Steps before writing to file + $createUiDefinitionOrder = "dataconnectors", "parsers", "workbooks", "analytics", "huntingqueries", "watchlists", "playbooks" + $baseCreateUiDefinition.parameters.steps = $baseCreateUiDefinition.parameters.steps | Sort-Object { $createUiDefinitionOrder.IndexOf($_.name) } + # Ensure single-step UI Definitions have proper type for steps + if ($($baseCreateUiDefinition.parameters.steps).GetType() -ne [System.Object[]]) { + $baseCreateUiDefinition.parameters.steps = @($baseCreateUiDefinition.parameters.steps) + } + $baseCreateUiDefinition | ConvertTo-Json -Depth $jsonConversionDepth | Out-File $createUiDefinitionOutputPath -Encoding utf8 + } + catch { + Write-Host "Failed to write output file $createUiDefinitionOutputPath" -ForegroundColor Red + break; + } + $zipPackageName = "$(if($contentToImport.Version){$contentToImport.Version}else{"newSolutionPackage"}).zip" + Compress-Archive -Path "$solutionFolder/*" -DestinationPath "$solutionFolder/$zipPackageName" -Force + + #downloading and running arm-ttk on generated solution + $armTtkFolder = "$PSScriptRoot/../arm-ttk" + if (!$(Get-Command Test-AzTemplate -ErrorAction SilentlyContinue)) { + Write-Output "Missing arm-ttk validations. Downloading module..." + Invoke-Expression "$armTtkFolder/download-arm-ttk.ps1" + } + Invoke-Expression "$armTtkFolder/run-arm-ttk-in-automation.ps1 '$solutionName'" +} \ No newline at end of file diff --git a/Tools/Create-Azure-Sentinel-Solution/V2/input/Solution_CiscoUmbrellaTemplateSpec.json b/Tools/Create-Azure-Sentinel-Solution/V2/input/Solution_CiscoUmbrellaTemplateSpec.json new file mode 100644 index 0000000000..37da09904d --- /dev/null +++ b/Tools/Create-Azure-Sentinel-Solution/V2/input/Solution_CiscoUmbrellaTemplateSpec.json @@ -0,0 +1,48 @@ +{ + "Name": "Cisco Umbrella", + "Author": "Microsoft - support@microsoft.com", + "Logo": "", + "Description": "The [Cisco Umbrella](https://umbrella.cisco.com/) solution for Microsoft Sentinel enables you to ingest [Cisco Umbrella events](https://docs.umbrella.com/deployment-umbrella/docs/log-formats-and-versioning) stored in Amazon S3 into Microsoft Sentinel using the Amazon S3 REST API.\n\n**Underlying Microsoft Technologies used:**\n\nThis solution takes a dependency on the following technologies, and some of these dependencies either may be in [Preview](https://azure.microsoft.com/support/legal/preview-supplemental-terms/) state or might result in additional ingestion or operational costs:\n\n\ta. [Azure Monitor HTTP Data Collector API](https://docs.microsoft.com/azure/azure-monitor/logs/data-collector-api)\n\n\tb. [Azure Functions](https://azure.microsoft.com/services/functions/#overview) ", + "WorkbookBladeDescription": "This Microsoft Sentinel Solution installs workbooks. Workbooks provide a flexible canvas for data monitoring, analysis, and the creation of rich visual reports within the Azure portal. They allow you to tap into one or many data sources from Microsoft Sentinel and combine them into unified interactive experiences.", + "AnalyticalRuleBladeDescription": "This solution installs the following analytic rule templates. After installing the solution, create and enable analytic rules in Manage solution view. ", + "HuntingQueryBladeDescription": "This solution installs the following hunting queries. After installing the solution, run these hunting queries to hunt for threats in Manage solution view", + "PlaybooksBladeDescription": "This solution installs the following Playbook templates. After installing the solution, playbooks can be managed in the Manage solution view. ", + "Data Connectors": [ + "DataConnectors/CiscoUmbrella/CiscoUmbrella_API_FunctionApp.json" + ], + "Parsers": [ + "Solutions/CiscoUmbrella/Parsers/Cisco_Umbrella" + ], + "Hunting Queries": [ + "Solutions/CiscoUmbrella/Hunting Queries/CiscoUmbrellaAnomalousFQDNsforDomain.yaml", + "Solutions/CiscoUmbrella/Hunting Queries/CiscoUmbrellaBlockedUserAgents.yaml", + "Solutions/CiscoUmbrella/Hunting Queries/CiscoUmbrellaDNSErrors.yaml", + "Solutions/CiscoUmbrella/Hunting Queries/CiscoUmbrellaDNSRequestsUunreliableCategory.yaml", + "Solutions/CiscoUmbrella/Hunting Queries/CiscoUmbrellaHighCountsOfTheSameBytesInSize.yaml", + "Solutions/CiscoUmbrella/Hunting Queries/CiscoUmbrellaHighValuesOfUploadedData.yaml", + "Solutions/CiscoUmbrella/Hunting Queries/CiscoUmbrellaPossibleConnectionC2.yaml", + "Solutions/CiscoUmbrella/Hunting Queries/CiscoUmbrellaPossibleDataExfiltration.yaml", + "Solutions/CiscoUmbrella/Hunting Queries/CiscoUmbrellaProxyAllowedUnreliableCategory.yaml", + "Solutions/CiscoUmbrella/Hunting Queries/CiscoUmbrellaRequestsUncategorizedURI.yaml" + ], + "Analytic Rules": [ + "Solutions/CiscoUmbrella/Analytic Rules/CiscoUmbrellaConnectionNon-CorporatePrivateNetwork.yaml", + "Solutions/CiscoUmbrella/Analytic Rules/CiscoUmbrellaConnectionToUnpopularWebsiteDetected.yaml", + "Solutions/CiscoUmbrella/Analytic Rules/CiscoUmbrellaCryptoMinerUserAgentDetected.yaml", + "Solutions/CiscoUmbrella/Analytic Rules/CiscoUmbrellaEmptyUserAgentDetected.yaml", + "Solutions/CiscoUmbrella/Analytic Rules/CiscoUmbrellaHackToolUserAgentDetected.yaml", + "Solutions/CiscoUmbrella/Analytic Rules/CiscoUmbrellaPowershellUserAgentDetected.yaml", + "Solutions/CiscoUmbrella/Analytic Rules/CiscoUmbrellaRareUserAgentDetected.yaml", + "Solutions/CiscoUmbrella/Analytic Rules/CiscoUmbrellaRequestAllowedHarmfulMaliciousURICategory.yaml", + "Solutions/CiscoUmbrella/Analytic Rules/CiscoUmbrellaRequestBlocklistedFileType.yaml", + "Solutions/CiscoUmbrella/Analytic Rules/CiscoUmbrellaURIContainsIPAddress.yaml" + ], + "Workbooks": [ + "Solutions/CiscoUmbrella/Workbooks/CiscoUmbrella.json" + ], + "BasePath": "C:\\GitHub\\Azure-Sentinel", + "Version": "2.0.0", + "Metadata": "SolutionMetadata.json", + "TemplateSpec": true, + "Is1PConnector": false +} \ No newline at end of file diff --git a/Tools/Create-Azure-Sentinel-Solution/V2/templating/SolutionAutomationInput.ts b/Tools/Create-Azure-Sentinel-Solution/V2/templating/SolutionAutomationInput.ts new file mode 100644 index 0000000000..ac29a2f675 --- /dev/null +++ b/Tools/Create-Azure-Sentinel-Solution/V2/templating/SolutionAutomationInput.ts @@ -0,0 +1,34 @@ +/** + * Solution Automation Input File Interface + * ----------------------------------------------------- + * The purpose of this interface is to provide detail on + * the various fields the input file can have. + */ + interface SolutionAutomationInput { + Name: string; //Solution Name - Ex. "Symantec Endpoint Protection" + Author: string; //Author of Solution - Ex. "Eli Forbes - v-eliforbes@microsoft.com" + Logo: string; //Link to the Logo used in the CreateUiDefinition.json + Description: string; //Solution Description used in the CreateUiDefinition.json + WorkbookDescription: string|string[]; //Workbook description(s) from ASI-Portal Workbooks Metadata + Version: string; //Package version to be created + //The following fields take arrays of paths relative to the solutions folder. + //Ex. Workbooks: ["Workbooks/SymantecEndpointProtection.json"] + Workbooks?: string[]; + WorkbookBladeDescription: string; //Description used in the CreateUiDefinition.json for Workbooks Blade + AnalyticalRuleBladeDescription: string; //Description used in the CreateUiDefinition.json for Analytical Rule Blade + HuntingQueryBladeDescription: string; //Description used in the CreateUiDefinition.json for Hunting Query Blade + PlaybooksBladeDescription: string; //Description used in the CreateUiDefinition.json for Playbook Blade + "Analytic Rules"?: string[]; + Playbooks?: string[]; + PlaybookDescription?: string|string[]; //Description used in the CreateUiDefinition.json + Parsers?: string[]; + SavedSearches?: string[]; + "Hunting Queries"?: string[]; + "Data Connectors"?: string[]; + Watchlists?: string[]; + WatchlistDescription?: string|string[]; //Description used in the CreateUiDefinition.json + BasePath?: string; //Optional base path to use. Either Internet URL or File Path. Default = "https://raw.githubusercontent.com/Azure/Azure-Sentinel/master/"" + Metadata: string; //Path to the SolutionMetadata file + TemplateSpec: true; + Is1PConnector: false; +} \ No newline at end of file diff --git a/Tools/Create-Azure-Sentinel-Solution/V2/templating/baseCreateUiDefinition.json b/Tools/Create-Azure-Sentinel-Solution/V2/templating/baseCreateUiDefinition.json new file mode 100644 index 0000000000..da7d97a984 --- /dev/null +++ b/Tools/Create-Azure-Sentinel-Solution/V2/templating/baseCreateUiDefinition.json @@ -0,0 +1,60 @@ +{ + "$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": "{{Logo}}\n\n**Note:** _There may be [known issues](https://aka.ms/sentinelsolutionsknownissues) pertaining to this Solution, please refer to them before installing._\n\n{{SolutionDescription}}\n\n{{DataConnectorCount}}{{ParserCount}}{{WorkbookCount}}{{AnalyticRuleCount}}{{HuntingQueryCount}}{{WatchlistCount}}{{LogicAppCustomConnectorCount}}{{PlaybookCount}}\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": [], + "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/Create-Azure-Sentinel-Solution/V2/templating/baseMainTemplate.json b/Tools/Create-Azure-Sentinel-Solution/V2/templating/baseMainTemplate.json new file mode 100644 index 0000000000..8a3fbfdd95 --- /dev/null +++ b/Tools/Create-Azure-Sentinel-Solution/V2/templating/baseMainTemplate.json @@ -0,0 +1,36 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "author": "{{author}}", + "comments": "Solution template for {{SolutionName}}" + }, + "parameters": { + + "location": { + "type": "string", + "minLength": 1, + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Not used, but needed to pass arm-ttk test `Location-Should-Not-Be-Hardcoded`. We instead use the `workspace-location` which is derived from the LA workspace" + } + }, + "workspace-location": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "[concat('Region to deploy solution resources -- separate from location selection',parameters('location'))]" + } + }, + "workspace": { + "defaultValue": "", + "type": "string", + "metadata": { + "description": "Workspace name for Log Analytics where Microsoft Sentinel is setup" + } + } + }, + "variables": {}, + "resources": [], + "outputs": {} +} diff --git a/Tools/Create-Azure-Sentinel-Solution/V2/templating/replaceLocationValue.js b/Tools/Create-Azure-Sentinel-Solution/V2/templating/replaceLocationValue.js new file mode 100644 index 0000000000..d099aa7ac5 --- /dev/null +++ b/Tools/Create-Azure-Sentinel-Solution/V2/templating/replaceLocationValue.js @@ -0,0 +1,13 @@ +/** replacePlaybookVarNames.js + * This small script is utilized to perform a global replacement of playbook variables within a string. + * This is necessary due to PowerShell not providing global match/replacement capability. + */ +const regexStr = /(resourceGroup\(\)\.location)/g; +const inputString = process.argv[2]; +const playbookNum = process.argv[3]; + +if (inputString.match(regexStr)) { + console.log(inputString.replace(regexStr, "parameters('workspace-location')")) +} else { + console.log(inputString); +} \ No newline at end of file diff --git a/Tools/Create-Azure-Sentinel-Solution/V2/templating/replacePlaybookParamNames.js b/Tools/Create-Azure-Sentinel-Solution/V2/templating/replacePlaybookParamNames.js new file mode 100644 index 0000000000..cee59d1f3a --- /dev/null +++ b/Tools/Create-Azure-Sentinel-Solution/V2/templating/replacePlaybookParamNames.js @@ -0,0 +1,13 @@ +/** replacePlaybookParamNames.js + * This small script is utilized to perform a global replacement of playbook parameter variables within a string. + * This is necessary due to PowerShell not providing global match/replacement capability. + */ +const regexStr = /parameters\(\'([\w\-\s]+)\'\)/g; +const inputString = process.argv[2]; +const playbookNum = process.argv[3]; + +if (inputString.match(regexStr)) { + console.log(inputString.replace(regexStr, `parameters('playbook${playbookNum}-$1')`)) +} else { + console.log(inputString); +} \ No newline at end of file diff --git a/Tools/Create-Azure-Sentinel-Solution/V2/templating/replacePlaybookVarNames.js b/Tools/Create-Azure-Sentinel-Solution/V2/templating/replacePlaybookVarNames.js new file mode 100644 index 0000000000..31fa4cd929 --- /dev/null +++ b/Tools/Create-Azure-Sentinel-Solution/V2/templating/replacePlaybookVarNames.js @@ -0,0 +1,13 @@ +/** replacePlaybookVarNames.js + * This small script is utilized to perform a global replacement of playbook variables within a string. + * This is necessary due to PowerShell not providing global match/replacement capability. + */ +const regexStr = /variables\(\'(\w+)\'\)/g; +const inputString = process.argv[2]; +const playbookNum = process.argv[3]; + +if (inputString.match(regexStr)) { + console.log(inputString.replace(regexStr, `variables('playbook${playbookNum}-$1')`)) +} else { + console.log(inputString); +} \ No newline at end of file diff --git a/Tools/Create-Azure-Sentinel-Solution/templating/SolutionAutomationInput.ts b/Tools/Create-Azure-Sentinel-Solution/templating/SolutionAutomationInput.ts index 5b0c82f65d..cdf7ce8f05 100644 --- a/Tools/Create-Azure-Sentinel-Solution/templating/SolutionAutomationInput.ts +++ b/Tools/Create-Azure-Sentinel-Solution/templating/SolutionAutomationInput.ts @@ -16,10 +16,13 @@ interface SolutionAutomationInput { Workbooks?: string[]; "Analytic Rules"?: string[]; Playbooks?: string[]; + PlaybookDescription?: string|string[]; //Description used in the CreateUiDefinition.json Parsers?: string[]; SavedSearches?: string[]; "Hunting Queries"?: string[]; "Data Connectors"?: string[]; Watchlists?: string[]; + WatchlistDescription?: string|string[]; //Description used in the CreateUiDefinition.json BasePath?: string; //Optional base path to use. Either Internet URL or File Path. Default = "https://raw.githubusercontent.com/Azure/Azure-Sentinel/master/"" + Metadata: string; //Path to the SolutionMetadata File } diff --git a/Tools/Create-Azure-Sentinel-Solution/templating/baseMainTemplate.json b/Tools/Create-Azure-Sentinel-Solution/templating/baseMainTemplate.json index b9655d43e2..8a3fbfdd95 100644 --- a/Tools/Create-Azure-Sentinel-Solution/templating/baseMainTemplate.json +++ b/Tools/Create-Azure-Sentinel-Solution/templating/baseMainTemplate.json @@ -26,7 +26,7 @@ "defaultValue": "", "type": "string", "metadata": { - "description": "Workspace name for Log Analytics where Sentinel is setup" + "description": "Workspace name for Log Analytics where Microsoft Sentinel is setup" } } }, diff --git a/Tools/CustomLogsIngestion-DCE-DCR/src/Send-AzMonitorCustomLogs.ps1 b/Tools/CustomLogsIngestion-DCE-DCR/src/Send-AzMonitorCustomLogs.ps1 index fdeaf7c53f..3171782c24 100644 --- a/Tools/CustomLogsIngestion-DCE-DCR/src/Send-AzMonitorCustomLogs.ps1 +++ b/Tools/CustomLogsIngestion-DCE-DCR/src/Send-AzMonitorCustomLogs.ps1 @@ -1,4 +1,4 @@ -<# +<# THE SCRIPT IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SCRIPT OR THE USE OR OTHER DEALINGS IN THE @@ -6,15 +6,15 @@ .SYNOPSIS Sends custom logs to a specific table in Azure Monitor. - + .DESCRIPTION Script to send data to a data collection endpoint which is a unique connection point for your subscription. The payload sent to Azure Monitor must be in JSON format. A data collection rule is needed in your Azure tenant that understands the format of the source data, potentially filters and transforms it for the target table, and then directs it to a specific table in a specific workspace. You can modify the target table and workspace by modifying the data collection rule without any change to the REST API call or source data. - + .PARAMETER LogPath Path to the log file or folder to read logs from and send them to Azure Monitor. - + .PARAMETER AADAppId Azure Active Directory application to authenticate against the API to send logs to Azure Monitor data collection endpoint. This script supports the Client Credential Grant Flow. @@ -24,22 +24,22 @@ .PARAMETER TenantId ID of Tenant - + .PARAMETER DcrImmutableId Immutable ID of the data collection rule used to process events flowing to an Azure Monitor data table. - + .PARAMETER DceURI Uri of the data collection endpoint used to host the data collection rule. .PARAMETER StreamName Name of stream to send data to before being procesed and sent to an Azure Monitor data table. - - .EXAMPLE - PS> Send-AzMonitorCustomLogs -LogPath C:\WinEvents.json -AADAppId 'XXXX' -AADAppSecret 'XXXXXX' -TenantId 'XXXXXX' -DcrImmutableId 'dcr-XXXX' -DceURI 'https://XXXX.westus2-1.ingest.monitor.azure.com' -StreamName 'Custom-WindowsEvent' - - .EXAMPLE + + .EXAMPLE + PS> Send-AzMonitorCustomLogs -LogPath C:\WinEvents.json -AADAppId 'XXXX' -AADAppSecret 'XXXXXX' -TenantId 'XXXXXX' -DcrImmutableId 'dcr-XXXX' -DceURI 'https://XXXX.westus2-1.ingest.monitor.azure.com' -StreamName 'Custom-WindowsEvent' + + .EXAMPLE PS> Send-AzMonitorCustomLogs -LogPath C:\WinEventsFolder\ -AADAppId 'XXXX' -AADAppSecret 'XXXXXX' -TenantId 'XXXXXX' -DcrImmutableId 'dcr-XXXX' -DceURI 'https://XXXX.westus2-1.ingest.monitor.azure.com' -StreamName 'Custom-WindowsEvent' - + .NOTES # Author: Roberto Rodriguez (@Cyb3rWard0g) # Modified: Sreedhar Ande @@ -47,22 +47,22 @@ # License: MIT # Reference: - # https://docs.microsoft.com/en-us/azure/azure-monitor/logs/custom-logs-overview - # https://docs.microsoft.com/en-us/azure/azure-monitor/logs/tutorial-custom-logs-api#send-sample-data + # https://docs.microsoft.com/azure/azure-monitor/logs/custom-logs-overview + # https://docs.microsoft.com/azure/azure-monitor/logs/tutorial-custom-logs-api#send-sample-data # https://securitytidbits.wordpress.com/2017/04/14/powershell-and-gzip-compression/ # Custom Logs Limit # Maximum size of API call: 1MB for both compressed and uncompressed data # Maximum data/minute per DCR: 1 GB for both compressed and uncompressed data. Retry after the duration listed in the Retry-After header in the response. - # Maximum requests/minute per DCR: 6,000. Retry after the duration listed in the Retry-After header in the response. + # Maximum requests/minute per DCR: 6,000. Retry after the duration listed in the Retry-After header in the response. #> -param( +param( [Parameter(Mandatory=$true)] - [ValidateScript({ + [ValidateScript({ if( -Not ($_ | Test-Path) ){ throw "File or folder does not exist" - } + } return $true })] [string]$LogPath, @@ -75,7 +75,7 @@ param( [Parameter(Mandatory=$true)] [string]$AADAppSecret, - + [Parameter(Mandatory=$true)] [string]$DcrImmutableId, @@ -89,11 +89,11 @@ param( #region HelperFunctions Function Write-Log { <# - .DESCRIPTION + .DESCRIPTION Write-Log is used to write information to a log file and to the console. - + .PARAMETER Severity - parameter specifies the severity of the log message. Values can be: Information, Warning, or Error. + parameter specifies the severity of the log message. Values can be: Information, Warning, or Error. #> [CmdletBinding()] @@ -102,18 +102,18 @@ Function Write-Log { [ValidateNotNullOrEmpty()] [string]$Message, [string]$LogFileName, - + [parameter()] [ValidateNotNullOrEmpty()] [ValidateSet('Information', 'Warning', 'Error')] [string]$Severity = 'Information' ) - # Write the message out to the correct channel + # Write the message out to the correct channel switch ($Severity) { "Information" { Write-Host $Message -ForegroundColor Green } "Warning" { Write-Host $Message -ForegroundColor Yellow } "Error" { Write-Host $Message -ForegroundColor Red } - } + } try { [PSCustomObject] [ordered] @{ Time = (Get-Date -f g) @@ -122,8 +122,8 @@ Function Write-Log { } | Export-Csv -Path "$PSScriptRoot\$LogFileName" -Append -NoTypeInformation -Force } catch { - Write-Error "An error occurred in Write-Log() method" -ErrorAction SilentlyContinue - } + Write-Error "An error occurred in Write-Log() method" -ErrorAction SilentlyContinue + } } #endregion @@ -133,7 +133,7 @@ Function Get-BearerToken { Try { Add-Type -AssemblyName System.Web Write-Log -Message "Obtaining Access Token" -LogFileName $LogFileName -Severity Information - $scope = [System.Web.HttpUtility]::UrlEncode("https://monitor.azure.com//.default") + $scope = [System.Web.HttpUtility]::UrlEncode("https://monitor.azure.com//.default") $body = "client_id=$AADAppId&scope=$scope&client_secret=$AADAppSecret&grant_type=client_credentials"; $headers = @{"Content-Type" = "application/x-www-form-urlencoded"}; $uri = "https://login.microsoftonline.com/$TenantID/oauth2/v2.0/token" @@ -149,19 +149,19 @@ Function Get-BearerToken { Function Send-DataToDCE { [CmdletBinding()] - param ( - [parameter(Mandatory = $true)] $JsonPayload, + param ( + [parameter(Mandatory = $true)] $JsonPayload, [parameter(Mandatory = $true)] $AccessToken, [parameter(Mandatory = $true)] $DceURI, [parameter(Mandatory = $true)] $DcrImmutableId, [parameter(Mandatory = $true)] $StreamName, [parameter(Mandatory = $true)] $ApiVersion ) - + # Initialize Headers and URI for POST request to the Data Collection Endpoint (DCE) $headers = @{"Authorization" = "Bearer $AccessToken"; "Content-Type" = "application/json"} $uri = "$DceURI/dataCollectionRules/$DcrImmutableId/streams/$StreamName`?api-version=$ApiVersion" - + Try { # Sending data to Data Collection Endpoint (DCE) -> Data Collection Rule (DCR) -> Azure Monitor table $IngestionStatus = Invoke-RestMethod -Uri $uri -Method "POST" -Body $JsonPayload -Headers $headers -verbose @@ -177,13 +177,13 @@ Function Send-DataToDCE { # Check Powershell version, needs to be 5 or higher if ($host.Version.Major -lt 5) { - Write-Log -Message "Supported PowerShell version for this script is 5 or above" -LogFileName $LogFileName -Severity Error + Write-Log -Message "Supported PowerShell version for this script is 5 or above" -LogFileName $LogFileName -Severity Error exit } $ApiVersion = "2021-11-01-preview" -$TimeStamp = Get-Date -Format yyyyMMdd_HHmmss +$TimeStamp = Get-Date -Format yyyyMMdd_HHmmss $LogFileName = '{0}_{1}.csv' -f "CustomlogsIngestion", $TimeStamp @@ -207,21 +207,21 @@ foreach ($file in $LogPath){ ################## $bearerToken = Get-BearerToken -foreach ($dataset in $all_datasets){ +foreach ($dataset in $all_datasets){ $extn = [IO.Path]::GetExtension($dataset) - if ($extn -ieq ".csv") { - $json_records = Get-Content $dataset | ConvertFrom-Csv | ConvertTo-Json - $json_payload= $json_records | Convertfrom-json | ConvertTo-Json + if ($extn -ieq ".csv") { + $json_records = Get-Content $dataset | ConvertFrom-Csv | ConvertTo-Json + $json_payload= $json_records | Convertfrom-json | ConvertTo-Json } else { $json_records = Get-Content $dataset $json_payload= $json_records | Convertfrom-json | ConvertTo-Json } - + $payload_size = ([System.Text.Encoding]::UTF8.GetBytes($json_payload).Length) If ($payload_size -le 1mb) { Write-Log -Message "Sending log events with size $dataset_size" -LogFileName $LogFileName -Severity Information - Send-DataToDCE -JsonPayload $json_payload -AccessToken $bearerToken -DceURI $DceURI -DcrImmutableId $DcrImmutableId -StreamName $StreamName -ApiVersion $ApiVersion + Send-DataToDCE -JsonPayload $json_payload -AccessToken $bearerToken -DceURI $DceURI -DcrImmutableId $DcrImmutableId -StreamName $StreamName -ApiVersion $ApiVersion } else { # Maximum size of API call: 1MB for both compressed and uncompressed data