Tools/Create-Azure-Sentinel-Solution/V2/createSolutionV2.ps1 (2,512 lines of code) (raw):
Import-Module powershell-yaml
Write-Host 'It is recommended to use createSolutionV3.ps1 file for solution packaging'
$jsonConversionDepth = 50
$path = "$PSScriptRoot\input"
$mainTemplateArtifact = [PSCustomObject]@{
name = "DefaultTemplate";
type = "Template"
}
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, $isWorkbook = $false) {
foreach ($prop in $resourceObj.PsObject.Properties) {
$key = $prop.Name
$val = $prop.Value
if ($null -eq $val) {
if ($isWorkbook)
{
$resourceObj.$key = ''
}
else
{
$resourceObj.$key = "[variables('blanks')]";
if (!$baseMainTemplate.variables.blanks) {
$baseMainTemplate.variables | Add-Member -NotePropertyName "blanks" -NotePropertyValue "[replace('b', 'b', '')]"
}
}
}
elseif ($val -is [System.Object[]]) {
if ($val.Count -eq 0) {
if ($isWorkbook)
{
$resourceObj.$key = @()
}
else
{
#$resourceObj.PsObject.Properties.Remove($key)
$resourceObj.$key = "[variables('TemplateEmptyArray')]";
if (!$baseMainTemplate.variables.TemplateEmptyArray) {
$baseMainTemplate.variables | Add-Member -NotePropertyName "TemplateEmptyArray" -NotePropertyValue "[json('[]')]"
}
}
}
else {
foreach ($item in $val) {
$itemIndex = $val.IndexOf($item)
$resourceObj.$key[$itemIndex] = $(removePropertiesRecursively $val[$itemIndex] $isWorkbook)
}
}
}
else {
if ($val -is [PSCustomObject]) {
if ($($val.PsObject.Properties).Count -eq 0) {
$resourceObj.PsObject.Properties.Remove($key)
}
else {
$resourceObj.$key = $(removePropertiesRecursively $val $isWorkbook)
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
}
}
}
function getParserDetails($solutionName)
{
$API = 'https://catalogapi.azure.com/offers?api-version=2018-08-01-beta&$filter=categoryIds%2Fany(cat%3A%20cat%20eq%20%27AzureSentinelSolution%27)%20or%20keywords%2Fany(key%3A%20contains(key%2C%27f1de974b-f438-4719-b423-8bf704ba2aef%27))'
$SolutionDataItems = $(Invoke-WebRequest -URI $API | ConvertFrom-Json).items
$parserResourceType = [PSObject]@{
templateSpecParserType = "Microsoft.OperationalInsights/workspaces/savedSearches"
workspaceType = "Microsoft.OperationalInsights/workspaces"
normalParserType = "savedSearches"
}
$variableExpressionRegex = "\[\s?variables\(\'_([\w\W]+)\'\)\s?\]"
$parserDisplayDetails = New-Object PSObject
$parserDisplayDetails | Add-Member -NotePropertyName "functionAlias" -NotePropertyValue $(getFileNameFromPath $file)
$parserDisplayDetails | Add-Member -NotePropertyName "displayName" -NotePropertyValue "$($fileName)"
$parserDisplayDetails | Add-Member -NotePropertyName "name" -NotePropertyValue "$($fileName)"
$currentSolution = $SolutionDataItems | Where-Object { $_.legacyId -eq $solutionName }
if($currentSolution.length -gt 0)
{
$templateUrl = ($currentSolution.plans[0].artifacts | Where-Object { (($_.name -eq $mainTemplateArtifact.name) -and ($_.type -eq $mainTemplateArtifact.type)) }).uri
$templateContent = $(Invoke-WebRequest -URI $templateUrl) | ConvertFrom-Json
if ($templateContent.resources -and $templateContent.variables) {
$templateVariables = $templateContent.variables
$parserTemplate = $templateContent.resources | Where-Object { $_.type -eq $parserResourceType.templateSpecParserType -or $_.type -eq $parserResourceType.workspaceType }
if ($null -ne $parserTemplate) {
if($null -ne $parserTemplate.resources)
{
$parserTemplate = $parserTemplate.resources | Where-Object {$_.properties.category -eq "Samples" -and $_.type -eq $parserResourceType.normalParserType }
}
$parserTemplate = $parserTemplate | Where-Object {$_.properties.functionAlias -eq $(getFileNameFromPath $file)}
if ($null -ne $parserTemplate) {
$parserDisplayDetails.functionAlias = $parserTemplate.properties.functionAlias;
$parserDisplayDetails.displayName = $parserTemplate.properties.displayName;
$parserDisplayDetails.name = $parserTemplate.name.split('/')[-1];
$suppressedOutput = $parserDisplayDetails.displayName -match $variableExpressionRegex
if ($suppressedOutput -and $matches[1]) {
$parserDisplayDetails.displayName = $templateVariables.$($matches[1])
}
$suppressedOutput = $parserDisplayDetails.name -match $variableExpressionRegex
if ($suppressedOutput -and $matches[1]) {
$parserDisplayDetails.name = $templateVariables.$($matches[1])
}
}
}
}
}
return $parserDisplayDetails;
}
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 = "$PSScriptRoot/../../../"
# 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 = @{};
$functionAppList = @{};
$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 solution installs workbook(s) to help you gain insights into the telemetry collected in Microsoft Sentinel. After installing the solution, start using the workbook in Manage solution view.";
}
},
[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
}
}
if($contentToImport.TemplateSpec) {
#Getting Workbook Metadata dependencies from Github
$workbookData = $null
$workbookFinalPath = $workbookMetadataPath + 'Workbooks/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 = @();
}
catch {
Write-Host "TemplateSpec Workbook Metadata Dependencies errors occurred: $($_.Exception.Message)" -ForegroundColor Red
break;
}
if($dependencies.source -and $dependencies.source.kind -and ($dependencies.source.kind -eq "Community" -or $dependencies.source.kind -eq "Standalone"))
{
throw "The file $fileName has metadata with source -> kind = Community | Standalone. Please remove it so that it can be packaged as a solution."
}
}
$workbookUIParameter = [PSCustomObject] @{ name = "workbook$workbookCounter"; type = "Microsoft.Common.Section"; label = $dependencies.title; elements = @( [PSCustomObject] @{ name = "workbook$workbookCounter-text"; type = "Microsoft.Common.TextBlock"; options = @{ text = $dependencies.description; } } ) }
$baseCreateUiDefinition.parameters.steps[$baseCreateUiDefinition.parameters.steps.Count - 1].elements += $workbookUIParameter
try {
$data = $rawData
# Serialize workbook data
$serializedData = $data | ConvertFrom-Json -Depth $jsonConversionDepth
# Remove empty braces
$serializedData = $(removePropertiesRecursively $serializedData $true) | 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 + 'Workbooks/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
}
if($null -ne $dataConnectorObject -or $null -ne $dataTypeObject){
$workbookDependencies = [PSCustomObject]@{
operator = "AND";
};
}
if($WorkbookDependencyCriteria.Count -gt 0)
{
$workbookDependencies | Add-Member -NotePropertyName "criteria" -NotePropertyValue $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 = "2022-02-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;
}
}
if($null -ne $workbookDependencies)
{
$workbookMetadata.properties | Add-Member -NotePropertyName "dependencies" -NotePropertyValue $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 = "2022-02-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) 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($fileName.ToLower() -eq "incident-trigger" -or $fileName.ToLower() -eq "alert-trigger" -or $fileName.ToLower() -eq "entity-trigger")
{
$parentPath = Split-Path $file -Parent; $fileName = (Split-Path $parentPath -Parent | Split-Path -leaf) + "-" + $fileName;
}
if($playbookData.metadata -and $playbookData.metadata.source -and $playbookData.metadata.source.kind -and ($playbookData.metadata.source.kind -eq "Community" -or $playbookData.metadata.source.kind -eq "Standalone"))
{
throw "The file $fileName has metadata with source -> kind = Community | Standalone. Please remove it so that it can be packaged as a solution."
}
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;
$IsFunctionAppResource = ($playbookData.resources | Where-Object {($_.type.ToLower() -eq "Microsoft.Web/sites".ToLower())}) ? $true : $false;
$DependencyCriteria += [PSCustomObject]@{
kind = $IsLogicAppsCustomConnector ? "LogicAppsCustomConnector" : $IsFunctionAppResource ? "AzureFunction" : "Playbook";;
contentId = "[variables('_$fileName')]";
version = "[variables('playbookVersion$playbookCounter')]";
};
if($fileName.ToLower() -match "FunctionApp"){
$functionAppsPlaybookId = $playbookData.parameters.FunctionAppName.defaultValue
}
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 Playbook templates to help implement your Security Orchestration, Automation and Response (SOAR) operations. After installing the solution, these will be deployed under Playbook Templates in the Automation blade in Microsoft Sentinel. They can be configured and managed from the Manage solution view in Content Hub.";
}
},
[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')]";
if (!$baseMainTemplate.variables.blanks) {
$baseMainTemplate.variables | Add-Member -NotePropertyName "blanks" -NotePropertyValue "[replace('b', 'b', '')]"
}
}
}
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("[")) -and ($resourceObj.$key -ne "[variables('TemplateEmptyArray')]"))
{
$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 = '';
$FunctionResource = @();
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 $false)
$playbookResource = $(addInternalSuffixRecursively $playbookResource)
$playbookResource = $(removeBlanksRecursively $playbookResource)
$playbookResources += $playbookResource;
$connectionCounter += 1
}
if(!$IsFunctionAppResource -and $rawData -like '*Microsoft.Web/sites*' )
{
if ($null -ne $playbookData -and $null -ne $playbookData.parameters){
foreach($param in $playbookData.parameters.PsObject.Properties)
{
if($param.Value -match "defaultValue" -and $functionAppList.ContainsKey($param.Value.defaultValue))
{
$playbookDependencies += [PSCustomObject] @{
kind = "AzureFunction";
contentId = $functionAppList[$param.Value.defaultValue].id;
version = $functionAppList[$param.Value.defaultValue].version;
}
}
}
}
}
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 -and !$IsFunctionAppResource) {
$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')))]" :
$IsFunctionAppResource ? "[concat(parameters('workspace'),'-fa-',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 = "2022-02-01";
name = "[variables('playbookTemplateSpecName$playbookCounter')]";
location = "[parameters('workspace-location')]";
tags = [PSCustomObject]@{
"hidden-sentinelWorkspaceId" = "[variables('workspaceResourceId')]";
"hidden-sentinelContentType" = $IsLogicAppsCustomConnector ? "LogicAppsCustomConnector"
: $IsFunctionAppResource ? "AzureFunction" : "Playbook";
};
properties = [PSCustomObject]@{
description = $IsLogicAppsCustomConnector -or $IsFunctionAppResource ? $playbookName : "$($playbookName) playbook";
displayName = $IsLogicAppsCustomConnector -or $IsFunctionAppResource ? $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')]"});
}
if ($IsFunctionAppResource) {
$functionAppList.add($functionAppsPlaybookId, @{ id="[variables('_$filename')]"; version="[variables('playbookVersion$playbookCounter')]"});
}
Write-Host $playbookDependencies
$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$playbookCounter'),'/'))))]" :
$IsFunctionAppResource ? "[[concat(variables('workspace-name'),'/Microsoft.SecurityInsights/',concat('AzureFunction-', last(split(variables('playbookId$playbookCounter'),'/'))))]" :
"[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('Playbook-', last(split(variables('playbookId$playbookCounter'),'/'))))]";
properties = [PSCustomObject]@{
parentId = $IsLogicAppsCustomConnector -or $IsFunctionAppResource ? "[[variables('playbookId$playbookCounter')]" : "[variables('playbookId$playbookCounter')]"
contentId = "[variables('_playbookContentId$playbookCounter')]";
kind = $IsLogicAppsCustomConnector ? "LogicAppsCustomConnector" : $IsFunctionAppResource ?
"AzureFunction" : "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$playbookCounter" -NotePropertyValue $fileName;
$playbookVariables | Add-Member -NotePropertyName "playbookId$playbookCounter" -NotePropertyValue "[[resourceId('Microsoft.Web/customApis', parameters('$logicAppsPlaybookId'))]"
}
if ($IsFunctionAppResource) {
$playbookVariables | Add-Member -NotePropertyName "playbookContentId$playbookCounter" -NotePropertyValue $fileName;
$playbookVariables | Add-Member -NotePropertyName "playbookId$playbookCounter" -NotePropertyValue "[[resourceId('Microsoft.Logic/workflows', variables('playbookContentId$playbookCounter'))]"
}
$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 = "2022-02-01";
name = "[concat(variables('playbookTemplateSpecName$playbookCounter'),'/',variables('playbookVersion$playbookCounter'))]";
location = "[parameters('workspace-location')]";
tags = [PSCustomObject]@{
"hidden-sentinelWorkspaceId" = "[variables('workspaceResourceId')]";
"hidden-sentinelContentType" = $IsLogicAppsCustomConnector ? "LogicAppsCustomConnector" : $IsFunctionAppResource ? "AzureFunction" : "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', '')]"
}
}
if ($null -ne $playbookTemplateSpecContent.properties.mainTemplate.metadata.entities -and
$playbookTemplateSpecContent.properties.mainTemplate.metadata.entities.count -le 0)
{
$playbookTemplateSpecContent.properties.mainTemplate.metadata.PSObject.Properties.Remove("entities");
}
if ($null -ne $playbookTemplateSpecContent.properties.mainTemplate.metadata.tags -and
$playbookTemplateSpecContent.properties.mainTemplate.metadata.tags.count -le 0)
{
$playbookTemplateSpecContent.properties.mainTemplate.metadata.PSObject.Properties.Remove("tags");
}
if ($null -ne $playbookTemplateSpecContent.properties.mainTemplate.metadata.prerequisites -and
[string]::IsNullOrWhitespace($playbookTemplateSpecContent.properties.mainTemplate.metadata.prerequisites))
{
$playbookTemplateSpecContent.properties.mainTemplate.metadata.PSObject.Properties.Remove("prerequisites");
}
$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 {
$ccpPollingConfig = [PSCustomObject] @{}
$ccpConnector = $false
$connectorData = ConvertFrom-Json $rawData
# If both ID and Title exist, is standard GenericUI data connector
if ($connectorData.resources -and
$connectorData.resources[0] -and
$connectorData.resources[0].properties -and
$connectorData.resources[0].properties.connectorUiConfig -and
$connectorData.resources[0].properties.pollingConfig) {
$ccpPollingConfig = $connectorData.resources[0].properties.pollingConfig
$ccpConnector = $true
$templateSpecConnectorData = $connectorData.resources[0].properties.connectorUiConfig
}
else
{
$ccpPollingConfig = $null
$ccpConnector = $false
$templateSpecConnectorData = $connectorData
}
}
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 $templateSpecConnectorData.instructionSteps) {
# Remove empty properties from each instructionStep
$stepIndex = $templateSpecConnectorData.instructionSteps.IndexOf($step)
$templateSpecConnectorData.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 ($templateSpecConnectorData.id -and $templateSpecConnectorData.title) {
$baseMainTemplate.variables | Add-Member -NotePropertyName "uiConfigId$connectorCounter" -NotePropertyValue $templateSpecConnectorData.id
$baseMainTemplate.variables | Add-Member -NotePropertyName "_uiConfigId$connectorCounter" -NotePropertyValue "[variables('uiConfigId$connectorCounter')]"
}
$baseMainTemplate.variables | Add-Member -NotePropertyName "dataConnectorContentId$connectorCounter" -NotePropertyValue $templateSpecConnectorData.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 $templateSpecConnectorData.metadata) ? "$($templateSpecConnectorData.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 = "2022-02-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 = $templateSpecConnectorData.instructionSteps
($instructionArray | ForEach {if($_.description -and $_.description.IndexOf('[Deploy To Azure]') -gt 0){$existingFunctionApp = $true;}})
if ($existingFunctionApp -eq $false)
{
# check if only instructions object is present without any description
foreach ($item in $instructionArray)
{
if ($null -eq $item.description -and $item.instructions.Count -gt 0)
{
foreach ($instructionItem in $item.instructions)
{
$parameterCount = $instructionItem.parameters.Count -gt 0
$parameterInstructionStepsCount = $instructionItem.parameters.instructionSteps.Count -gt 0
if ($parameterCount -and $parameterInstructionStepsCount)
{
foreach ($desc in $instructionItem.parameters.instructionSteps)
{
if ($desc.description && $desc.description.IndexOf('Deploy To Azure') -gt 0)
{
$existingFunctionApp = $true
break
}
}
}
}
}
}
}
if($existingFunctionApp)
{
$templateSpecConnectorData.title = ($templateSpecConnectorData.title.Contains("using Azure Functions")) ? $templateSpecConnectorData.title : $templateSpecConnectorData.title + " (using Azure Functions)"
#$templateSpecConnectorData.title = $templateSpecConnectorData.title + " (using Azure Functions)";
}
}
# Data Connector Content -- *Assumes GenericUI
if($contentToImport.Is1PConnector)
{
$1pconnectorData = $templateSpecConnectorData
$1pconnectorData = $1pconnectorData | Select-Object -Property id,title,publisher,descriptionMarkdown, graphQueries, connectivityCriterias,dataTypes
}
$templateSpecConnectorUiConfig = ($contentToImport.Is1PConnector -eq $true) ? $1pconnectorData : $templateSpecConnectorData
$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" : (($ccpConnector -eq $true) ? $connectorData.resources[0].kind : "GenericUI");
properties = [PSCustomObject]@{
connectorUiConfig = $templateSpecConnectorUiConfig
}
}
if($null -ne $ccpPollingConfig)
{
$dataConnectorContent.properties | Add-Member -NotePropertyName "pollingConfig" -NotePropertyValue $ccpPollingConfig
}
$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 = "2022-02-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')]";
}
}
# This section is for CCP connectors
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')
$connectorUiConfig.id = if ($contentToImport.TemplateSpec) { "[variables('_uiConfigId$connectorCounter')]" }else { "[variables('_connector$connectorCounter-source')]" };
$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))
$connectorDescriptionText = "This Solution installs the data connector for $solutionName. You can get $solutionName $connectorDataType data in your Microsoft Sentinel workspace. After installing the solution, configure and enable this data connector by following guidance in Manage solution view."
$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."
$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 = "$($watchlistData.properties.watchlistAlias)"; 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
}
if($yaml.metadata -and $yaml.metadata.source -and $yaml.metadata.source.kind -and ($yaml.metadata.source.kind -eq "Community" -or $yaml.metadata.source.kind -eq "Standalone"))
{
throw "The file $fileName has metadata with source -> kind = Community | Standalone. Please remove it so that it can be packaged as a solution."
}
$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.Trim();
if($huntingQueryDescription.StartsWith("'"))
{
$huntingQueryDescription = $huntingQueryDescription.substring(1, $huntingQueryDescription.length-1)
}
if($huntingQueryDescription.EndsWith("'"))
{
$huntingQueryDescription = $huntingQueryDescription.substring(0, $huntingQueryDescription.length-1)
}
$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 = "2022-02-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";
}
}
if($baseHuntingQueryTemplateSpec.properties.displayName.length -ge 64){
$baseHuntingQueryTemplateSpec.properties.displayName = "$($solutionName) HQ 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 = "2022-02-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."
$dependencyDescription = "This hunting query depends on $($yaml.requiredDataConnectors.connectorId) data connector ($($($yaml.requiredDataConnectors.dataTypes)) Parser or Table)"
}
$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;
}
if($yaml.metadata -and $yaml.metadata.source -and $yaml.metadata.source.kind -and ($yaml.metadata.source.kind -eq "Community" -or $yaml.metadata.source.kind -eq "Standalone"))
{
throw "The file $fileName has metadata with source -> kind = Community | Standalone. Please remove it so that it can be packaged as a solution."
}
$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 -and $yaml.requiredDataConnectors.count -gt 0)
{
$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;
}
}
else
{
$alertRule | Add-Member -NotePropertyName requiredDataConnectors -NotePropertyValue "[variables('TemplateEmptyArray')]";
if (!$baseMainTemplate.variables.TemplateEmptyArray) {
$baseMainTemplate.variables | Add-Member -NotePropertyName "TemplateEmptyArray" -NotePropertyValue "[json('[]')]"
}
}
if (!$yaml.severity) {
$alertRule.severity = "Medium"
}
# Content Modifications
$triggerOperators = [PSCustomObject] @{ gt = "GreaterThan" ; lt = "LessThan" ; eq = "Equal" ; ne = "NotEqual"; GreaterThan = "GreaterThan" ; LessThan = "LessThan" ; Equal = "Equal" ; NotEqual = "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
}
if ($yaml.relevantTechniques -and ($yaml.relevantTechniques.Count -gt 0) ) {
if ($yaml.relevantTechniques -match ' ') {
$yaml.relevantTechniques = $yaml.relevantTechniques -replace ' ', ''
}
$alertRule | Add-Member -NotePropertyName techniques -NotePropertyValue ([array]($yaml.relevantTechniques | ForEach-Object { ($_ -split "\.")[0] })) # Add relevantTechniques 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"
}
}
function Remove-EmptyArrays($Object) {
(@($Object).GetEnumerator() | ? {
if($_.GetType().fullname -eq "System.Collections.Hashtable"){
-not $_.Values
}
else
{
-not $_.Value
}
}) |
% {
$Object.Remove($_.Name)
}
return $Object;
}
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", "incidentConfiguration", "sentinelEntitiesMappings")) {
if ($yaml.$yamlField) {
$alertRule | Add-Member -MemberType NoteProperty -Name $yamlField -Value $(Remove-EmptyArrays $yaml.$yamlField)
if($yamlField -eq "entityMappings" -and $yaml.$yamlField.length -lt 2)
{
$alertRule.entityMappings = @($alertRule.entityMappings);
}
}
}
# 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 = "2022-02-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";
}
}
if($baseAnalyticRuleTemplateSpec.properties.displayName.length -ge 64){
$baseAnalyticRuleTemplateSpec.properties.displayName = "$($solutionName) AR 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 = "2022-02-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) {
$displayDetails = getParserDetails $solutionId
$baseMainTemplate.variables | Add-Member -NotePropertyName "parserName$parserCounter" -NotePropertyValue "$($displayDetails.name)"
$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 = "2022-02-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 = "$($displayDetails.displayName)"
category = "Samples"
functionAlias = "$($displayDetails.functionAlias)"
query = "$content"
version = 1
tags = @([PSCustomObject]@{
"name" = "description"
"value" = "$($displayDetails.displayName)"
};
)
}
}
$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('_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]@{
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 = "2022-02-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 = "$($displayDetails.displayName)";
category = "Samples";
functionAlias = "$($displayDetails.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'"
}