Tools/Create-Azure-Sentinel-Solution/common/commonFunctions.ps1 (3,350 lines of code) (raw):

$jsonConversionDepth = 50 $mainTemplateArtifact = [PSCustomObject]@{ name = "DefaultTemplate"; type = "Template" } # Base JSON Object Paths $baseMainTemplatePath = "$PSScriptRoot/templating/baseMainTemplate.json" $baseCreateUiDefinitionPath = "$PSScriptRoot/templating/baseCreateUiDefinition.json" $global:baseMainTemplate = Get-Content -Raw $baseMainTemplatePath | Out-String | ConvertFrom-Json $global:baseCreateUiDefinition = Get-Content -Raw $baseCreateUiDefinitionPath | Out-String | ConvertFrom-Json # Content Counters - (for adding numbering to each item) $global:analyticRuleCounter = 1 $global:connectorCounter = 1 $global:workbookCounter = 1 $global:playbookCounter = 1 $global:parserCounter = 1 $global:savedSearchCounter = 1 $global:huntingQueryCounter = 1 $global:watchlistCounter = 1 $global:DependencyCriteria = @(); $global:customConnectorsList = @{}; $global:functionAppList = @{}; $ContentKindDict = [System.Collections.Generic.Dictionary[String,string]]::new() $ContentKindDict.Add("AnalyticsRule", "ar") $ContentKindDict.Add("AnalyticsRuleTemplate", "art") $ContentKindDict.Add("DataConnector", "dc") $ContentKindDict.Add("DataType", "dt") $ContentKindDict.Add("HuntingQuery", "hq") $ContentKindDict.Add("InvestigationQuery", "iq") $ContentKindDict.Add("Parser", "pr") $ContentKindDict.Add("Playbook", "pl") $ContentKindDict.Add("PlaybookTemplate", "plt") $ContentKindDict.Add("Workbook", "wb") $ContentKindDict.Add("WorkbookTemplate", "wbt") $ContentKindDict.Add("Watchlist", "wl") $ContentKindDict.Add("WatchlistTemplate", "wlt") $ContentKindDict.Add("Notebook", "nb") $ContentKindDict.Add("Solution", "sl") $ContentKindDict.Add("AzureFunction", "fa") $ContentKindDict.Add("LogicAppsCustomConnector", "lc") $ContentKindDict.Add("AutomationRule", "ar") $ContentKindDict.Add("ResourcesDataConnector", "rdc") $ContentKindDict.Add("Standalone", "sa") function ReadFileContent($filePath) { try { if (!(Test-Path -Path "$filePath")) { return $null; } $stream = New-Object System.IO.StreamReader -Arg "$filePath"; $content = $stream.ReadToEnd(); $stream.Close(); if ($null -eq $content) { Write-Host "Error in reading file $filePath" return $null; } else { $fileContent = $content | ConvertFrom-Json; return $fileContent; } } catch { Write-Host "Error occured in ReadFileContent. Error details : $_" return $null; } } 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 addBlankVariables($resourceObj) { $resourceObj.$key = "[variables('blanks')]"; if (!$global:baseMainTemplate.variables.blanks) { $global:baseMainTemplate.variables | Add-Member -NotePropertyName "blanks" -NotePropertyValue "[replace('b', 'b', '')]" } } function addEmptyArrayVariables($resourceObj) { $resourceObj.$key = "[variables('TemplateEmptyArray')]"; if (!$global:baseMainTemplate.variables.TemplateEmptyArray) { $global:baseMainTemplate.variables | Add-Member -NotePropertyName "TemplateEmptyArray" -NotePropertyValue "[json('[]')]" } } function addEmptyObjectVariables($resourceObj) { $resourceObj.$key = "[variables('TemplateEmptyObject')]"; if (!$global:baseMainTemplate.variables.TemplateEmptyObject) { $global:baseMainTemplate.variables | Add-Member -NotePropertyName "TemplateEmptyObject" -NotePropertyValue "[json('{}')]" } } function addNullVariables($resourceObj) { $resourceObj.$key = "[variables('TemplateNullVariable')]"; if (!$global:baseMainTemplate.variables.TemplateNullVariable) { $global:baseMainTemplate.variables | Add-Member -NotePropertyName "TemplateNullVariable" -NotePropertyValue "null" } } function IsSkipActionInputs($resourceObj, $key, $val, $isAction = $false) { if ($isAction -and $key -eq "inputs" -and $val -is [System.Management.Automation.PSCustomObject] -and -not "$val") { # when action has "inputs": {} addEmptyObjectVariables $resourceObj return $true } if ($isAction -and $key -eq "inputs" -and $null -ne $val -and $val -is [System.Array] -and $val.count -eq 0) { # when action has "inputs": [] addEmptyArrayVariables $resourceObj return $true } if ($isAction -and $key -eq "inputs" -and $null -eq $val) { # when action has "inputs": null addNullVariables $resourceObj return $true } return $false } function IsSkipActionInputsValue($resourceObj, $key, $val, $isAction = $false, $isInputs = $false) { if ($isAction -and $isInputs -and $key -eq "value" -and $val -is [System.Management.Automation.PSCustomObject] -and -not "$val") { # when action has inputs--> "value": {} addEmptyObjectVariables $resourceObj continue } if ($isAction -and $isInputs -and $key -eq "value" -and $null -ne $val -and $val -is [System.Array] -and $val.count -eq 0) { # when action has inputs--> "value": [] addEmptyArrayVariables $resourceObj continue } if ($isAction -and $isInputs -and $key -eq "value" -and $null -eq $val) { # when action has inputs--> "value": null addNullVariables $resourceObj continue } } function removePropertiesRecursively ($resourceObj, $isWorkbook = $false, $isAction = $false, $isInputs = $false) { foreach ($prop in $resourceObj.PsObject.Properties) { $key = $prop.Name $val = $prop.Value if ($key -eq 'actions') { $isAction = $true } if ($key -eq 'inputs') { $isInputs = $true } # if actions --> inputs has either {} or [] or null then we skip later code if (IsSkipActionInputs $resourceObj $key $val $isAction) { continue } # if actions --> inputs --> value has either {} or [] or null then we skip later code if (IsSkipActionInputsValue $resourceObj $key $val $isAction $isInputs) { continue } if ($null -eq $val) { if ($isWorkbook) { #$resourceObj.$key = '' $resourceObj.PsObject.Properties.Remove($key) } else { if ($isAction -and $isInputs) { continue } else { $resourceObj.$key = "[variables('blanks')]"; if (!$global:baseMainTemplate.variables.blanks) { $global:baseMainTemplate.variables | Add-Member -NotePropertyName "blanks" -NotePropertyValue "[replace('b', 'b', '')]" } } } } elseif ($val -is [System.Object[]]) { if ($val.Count -eq 0) { if ($isWorkbook) { #$resourceObj.$key = @() $resourceObj.PsObject.Properties.Remove($key) } else { $resourceObj.$key = "[variables('TemplateEmptyArray')]"; if (!$global:baseMainTemplate.variables.TemplateEmptyArray) { $global:baseMainTemplate.variables | Add-Member -NotePropertyName "TemplateEmptyArray" -NotePropertyValue "[json('[]')]" } } } else { foreach ($item in $val) { $itemIndex = $val.IndexOf($item) $resourceObj.$key[$itemIndex] = $(removePropertiesRecursively $val[$itemIndex] $isWorkbook $isAction $isInputs) } } } else { if ($val -is [PSCustomObject]) { if ($($val.PsObject.Properties).Count -eq 0) { if ($key -eq "dataSources") { $resourceObj.$key = "[variables('TemplateEmptyObject')]"; if (!$global:baseMainTemplate.variables.TemplateEmptyArray) { $global:baseMainTemplate.variables | Add-Member -NotePropertyName "TemplateEmptyObject" -NotePropertyValue "[json('{}')]" } } else { $resourceObj.PsObject.Properties.Remove($key) } } else { $resourceObj.$key = $(removePropertiesRecursively $val $isWorkbook $isAction $isInputs) if ($($resourceObj.$key.PsObject.Properties).Count -eq 0) { $resourceObj.PsObject.Properties.Remove($key) } } } elseif ($key -eq 'query' -and $isWorkbook -eq $true) { try { # this means its an json array $isValidJsonStr = $val | Test-Json -ErrorAction Ignore if ($isValidJsonStr) { $queryObj = ConvertFrom-Json $val -ErrorAction Stop; foreach ($propItem in $queryObj.PsObject.Properties) { if ($null -eq $propItem.Value -or $propItem.Value -eq '[]') { $queryObj.PsObject.Properties.Remove($propItem.Name) } } $resourceObj.$key = $queryObj | ConvertTo-Json -Compress -Depth $jsonConversionDepth | Out-String } } catch { } } } } $resourceObj } function queryResourceExists () { foreach ($resource in $global:baseMainTemplate.resources) { if ($resource.type -eq "Microsoft.OperationalInsights/workspaces") { return $true } } return $false } function getQueryResourceLocation () { for ($i = 0; $i -lt $global:baseMainTemplate.resources.Length; $i++) { if ($global:baseMainTemplate.resources[$i].type -eq "Microsoft.OperationalInsights/workspaces") { return $i } } } function getParserDetails($solutionName,$yaml,$isyaml) { $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 $functionAlias = ($isyaml -eq $true) ? $yaml.FunctionName : $(getFileNameFromPath $file) $displayName = ($isyaml -eq $true) ? "$($yaml.Function.Title)" : "$($fileName)" $name = ($isyaml -eq $true) ? "$($yaml.FunctionName)" : "$($fileName)" $parserDisplayDetails | Add-Member -NotePropertyName "functionAlias" -NotePropertyValue $functionAlias $parserDisplayDetails | Add-Member -NotePropertyName "displayName" -NotePropertyValue $displayName $parserDisplayDetails | Add-Member -NotePropertyName "name" -NotePropertyValue $name $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) { $parserTemplate = $parserTemplate | Where-Object {$null -ne $_.properties.category -and $_.type -like "*savedSearches*" } } $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]; $parserNameValue = $parserTemplate.name if ($parserNameValue -like "*concat(parameters('workspace')*") { $splitParserObjectName = $parserNameValue -split "," $parserActualName = $splitParserObjectName[2].Replace("'", "").Replace(")]", "") $parserDisplayDetails.name = $parserActualName } if ($parserNameValue -like "*variables('parserObject*") { $splitParserObjectName = $parserNameValue -split "parserObject" $lastSingleQuoteIndex = $splitParserObjectName[1].LastIndexOf("'"); $parserCountValue = $splitParserObjectName[1].Substring(0, $lastSingleQuoteIndex) $parserActualName = $templateVariables."parserObject$parserCountValue"."_parserName$parserCountValue" $replaceConcatValue = $parserActualName.LastIndexOf(",'") + 2 $lastIndexOfQuotation = $parserActualName.LastIndexOf("'") $finalizedParserName = $parserActualName.Substring($replaceConcatValue, $lastIndexOfQuotation - $replaceConcatValue) $parserDisplayDetails.name = $finalizedParserName; } $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; } 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 } function checkResourceCounts ($countList) { if ($countList -isnot [System.Array]) { return $false } else { foreach ($count in $countList) { if ($count -gt 0) { return $true } } return $false } } function updateDescriptionCount($counter, $emplaceString, $replaceString, $countStringCondition) { if ($counter -gt 0) { $ruleCountSubstring = "$emplaceString$counter" $ruleCountString = $(if ($countStringCondition) { "$ruleCountSubstring, " } else { $ruleCountSubstring }) $global:baseCreateUiDefinition.parameters.config.basics.description = $global:baseCreateUiDefinition.parameters.config.basics.description -replace $replaceString, $ruleCountString } else { $global:baseCreateUiDefinition.parameters.config.basics.description = $global:baseCreateUiDefinition.parameters.config.basics.description -replace $replaceString, "" } } function GetContentSchemaVersion($defaultPackageVersion, $dataInputVersion) { # DEPENDING OF VERSION WE SHOULD SET THE CONTENTSCHEMAVERSION if ($null -eq $defaultPackageVersion -and $null -eq $dataInputVersion) { # WHEN BOTH NULL $newMetadata.Properties | Add-Member -Name 'contentSchemaVersion' -Type NoteProperty -Value "3.0.0"; } elseif ($null -ne $defaultPackageVersion -and $null -ne $dataInputVersion -and ($dataInputVersion -ne $defaultPackageVersion)) { # when both has version $inputMajor = $dataInputVersion.split(".")[0] $defaultMajor = $defaultPackageVersion.split(".")[0] if (($inputMajor -ge $defaultMajor -and $defaultMajor -ge 3) -or $defaultMajor -ge 3) { $newMetadata.Properties | Add-Member -Name 'contentSchemaVersion' -Type NoteProperty -Value "3.0.0"; } else { $newMetadata.Properties | Add-Member -Name 'contentSchemaVersion' -Type NoteProperty -Value "2.0.0"; } } elseif ($null -eq $defaultPackageVersion -and $null -eq $dataInputVersion) { # when both null $newMetadata.Properties | Add-Member -Name 'contentSchemaVersion' -Type NoteProperty -Value "3.0.0"; Write-Host "contentSchemaVersion set is 3.0.0 as both defaultPackageVersion and version field are null" } elseif ($null -ne $defaultPackageVersion -and $null -eq $dataInputVersion) { # when data input is null and default is not null $major = $defaultPackageVersion.split(".")[0] $newMetadata.Properties | Add-Member -Name 'contentSchemaVersion' -Type NoteProperty -Value "$major.0.0"; Write-Host "contentSchemaVersion set is $major inside of elseif where defaultPackageVersion is not null but input file version is null" } elseif ($null -eq $defaultPackageVersion -and $null -ne $dataInputVersion) { # when data input is not null but default is null $inputMajor = $dataInputVersion.split(".")[0] $newMetadata.Properties | Add-Member -Name 'contentSchemaVersion' -Type NoteProperty -Value "$inputMajor.0.0"; Write-Host "contentSchemaVersion inputMajor set is $inputMajor inside of elseif where defaultPackageVersion is null but input file version is not null" } elseif ($null -ne $defaultPackageVersion -and $null -ne $dataInputVersion -and $dataInputVersion -ne $defaultPackageVersion) { $inputMajor = $dataInputVersion.split(".")[0] $defaultMajor = $defaultPackageVersion.split(".")[0] if ($inputMajor -gt $defaultMajor -or $inputMajor -eq $defaultMajor) { $newMetadata.Properties | Add-Member -Name 'contentSchemaVersion' -Type NoteProperty -Value "$inputMajor.0.0"; } elseif ($inputMajor -gt $defaultMajor) { $newMetadata.Properties | Add-Member -Name 'contentSchemaVersion' -Type NoteProperty -Value "$defaultMajor.0.0"; } } elseif ($defaultPackageVersion -eq $dataInputVersion) { # WHEN BOTH SAME if ($null -eq $defaultPackageVersion) { $newMetadata.Properties | Add-Member -Name 'contentSchemaVersion' -Type NoteProperty -Value "3.0.0"; } else { $major = $defaultPackageVersion.split(".")[0] $newMetadata.Properties | Add-Member -Name 'contentSchemaVersion' -Type NoteProperty -Value "$major.0.0"; } } else { $newMetadata.Properties | Add-Member -Name 'contentSchemaVersion' -Type NoteProperty -Value "3.0.0"; } return $newMetadata } function PrepareSolutionMetadata($solutionMetadataRawContent, $contentResourceDetails, $defaultPackageVersion = $null) { Write-Host "Inside of PrepareSolutionMetadata" $json = $solutionMetadataRawContent # Create Metadata Resource Object if ($json.support) { $support = $json.support; } if ($json.categories) { $categories = $json.categories; } $Author = $contentToImport.Author.Split(" - "); $newMetadata = [PSCustomObject]@{ type = $contentResourceDetails.metadata #"Microsoft.OperationalInsights/workspaces/providers/metadata"; apiVersion = $contentResourceDetails.metadataApiVersion; location = "[parameters('workspace-location')]"; properties = [PSCustomObject] @{ version = $contentToImport.Version; kind = "Solution"; }; }; if($contentToImport.TemplateSpec) { $hasVersionAttribute = [bool]($contentToImport.PSobject.Properties.name -match "Version") if ($hasVersionAttribute) { $newMetadata = GetContentSchemaVersion -defaultPackageVersion $defaultPackageVersion -dataInputVersion $contentToImport.Version } else { $newMetadata = GetContentSchemaVersion -defaultPackageVersion $defaultPackageVersion -dataInputVersion $contentToImport.Version } } if($contentResourceDetails.apiVersion -eq '3.0.0') { $newMetadata.Properties | Add-Member -Name 'displayName' -Type NoteProperty -Value $contentToImport.Name; $newMetadata.Properties | Add-Member -Name 'publisherDisplayName' -Type NoteProperty -Value $(If ($support.psobject.properties["tier"].value.tolower() -eq "microsoft") {"Microsoft Sentinel, $($support.psobject.properties["name"].value)"} Else {$($support.psobject.properties["name"].value)}) $newMetadata.Properties | Add-Member -Name 'descriptionHtml' -Type NoteProperty -Value $contentToImport.Description; $newMetadata.Properties | Add-Member -Name 'contentKind' -Type NoteProperty -Value "Solution"; $global:baseMainTemplate.variables | Add-Member -NotePropertyName "_solutioncontentProductId" -NotePropertyValue "[concat(take(variables('_solutionId'),50),'-','$($ContentKindDict.ContainsKey("Solution") ? $ContentKindDict["Solution"] : '')','-', uniqueString(concat(variables('_solutionId'),'-','Solution','-',variables('_solutionId'),'-', variables('_solutionVersion'))))]" $newMetadata.Properties | Add-Member -Name 'contentProductId' -Type NoteProperty -Value "[variables('_solutioncontentProductId')]" $newMetadata.Properties | Add-Member -Name 'id' -Type NoteProperty -Value "[variables('_solutioncontentProductId')]" $newMetadata.Properties | Add-Member -Name 'icon' -Type NoteProperty -Value $contentToImport.Logo; } $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 ($global: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; } $hasDependentDomainSolutionIdsProp = [bool]($contentToImport.PSobject.Properties.Name.ToLower() -match "dependentdomainsolutionids") if ($hasDependentDomainSolutionIdsProp -and $contentToImport.dependentDomainSolutionIds.Length -gt 0) { if ($global:DependencyCriteria.Count -gt 0) { # add solution dependencies foreach($sid in $contentToImport.dependentDomainSolutionIds){ $global:DependencyCriteria += [PSCustomObject]@{ kind = "Solution"; contentId = "$sid"; }; } $dependencies = [PSCustomObject]@{ criteria = $global:DependencyCriteria; }; $newMetadata.properties | Add-Member -Name 'dependencies' -Type NoteProperty -Value $dependencies; } } else { if ($global:DependencyCriteria.Count -gt 0) { $dependencies = [PSCustomObject]@{ operator = "AND"; criteria = $global: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; } 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; } if ($null -ne $categoriesDetails) { $newMetadata.properties | Add-Member -Name 'categories' -Type NoteProperty -Value $categoriesDetails; } $global:baseMainTemplate.resources += $newMetadata; ### Removing the Non-Sentinel Resources if there are any: $newobject = $global:baseMainTemplate.resources | ? {$_.type -in $contentResourceDetails.resources} $global:baseMainTemplate.resources = $newobject } function GetWorkbookDataMetadata($file, $isPipelineRun, $contentResourceDetails, $baseFolderPath, $contentToImport) { Write-Host "Generating Workbook using $file" $solutionRename = Replace-SpecialChars -InputString $solutionName -Type 'solutionname' $fileName = Split-Path $file -leafbase; $indexOfOpenBraces = $fileName.IndexOf('(') if ($indexOfOpenBraces -le 0) { $fileName = Replace-SpecialChars -InputString $fileName -Type 'filename' } $workbookKey = $fileName; $fileName = $fileName + "Workbook"; if ($global:workbookCounter -eq 1) { # Add workbook source variables if (!$contentToImport.TemplateSpec){ $global:baseMainTemplate.variables | Add-Member -NotePropertyName "workbook-source" -NotePropertyValue "[concat(resourceGroup().id, '/providers/Microsoft.OperationalInsights/workspaces/',parameters('workspace'))]" $global: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" } } } ) } $global: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"; } } $global:baseMainTemplate.parameters | Add-Member -MemberType NoteProperty -Name "formattedTimeNow" -Value $timeNowParameter } } $workbookFinalPath = $baseFolderPath + 'Workbooks/WorkbooksMetadata.json'; # BELOW IS THE NEW CODE ADDED FROM AZURE SENTINEL REPO if($contentToImport.TemplateSpec) { #Getting Workbook Metadata dependencies from Github $workbookData = $null #$workbookFinalPath = $workbookMetadataPath + 'Tools/Create-Azure-Sentinel-Solution/V2/WorkbookMetadata/WorkbooksMetadata.json'; try { Write-Host "Downloading $workbookFinalPath" $workbookData = (New-Object System.Net.WebClient).DownloadString($workbookFinalPath) $dependencies = $workbookData | ConvertFrom-Json | Where-Object {($_.templateRelativePath.split('.')[0].ToLower() -eq $workbookKey.ToLower())} $WorkbookDependencyCriteria = @(); } 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$global:workbookCounter"; type = "Microsoft.Common.Section"; label = $dependencies.title; elements = @( [PSCustomObject] @{ name = "workbook$global:workbookCounter-text"; type = "Microsoft.Common.TextBlock"; options = @{ text = $dependencies.description; } } ) } $global:baseCreateUiDefinition.parameters.steps[$global: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[$global:workbookCounter - 1] } elseif ($contentToImport.WorkbookDescription -and $contentToImport.WorkbookDescription -is [System.String]) { $contentToImport.WorkbookDescription } else { "" }) #creating parameters in mainTemplate $workbookIDParameterName = "workbook$global:workbookCounter-id" $workbookNameParameterName = "workbook$global:workbookCounter-name" $workbookIDParameter = [PSCustomObject] @{ type = "string"; defaultValue = "[newGuid()]"; minLength = 1; metadata = [PSCustomObject] @{ description = "Unique id for the workbook" }; } if(!$contentToImport.TemplateSpec) { $global:baseMainTemplate.parameters | Add-Member -MemberType NoteProperty -Name $workbookIDParameterName -Value $workbookIDParameter } # Create Workbook Resource Object $newWorkbook = [PSCustomObject]@{ type = "Microsoft.Insights/workbooks"; name = "[parameters('workbook$global:workbookCounter-id')]"; location = "[parameters('workspace-location')]"; kind = "shared"; apiVersion = $contentResourceDetails.insightsWorkbookApiVersion; #"2021-08-01"; metadata = [PSCustomObject]@{}; properties = [PSCustomObject] @{ displayName = $contentToImport.Workbooks ? "[parameters('workbook$global:workbookCounter-name')]" : "[concat(parameters('workbook$global: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 try { Write-Host "Downloading $workbookFinalPath" $workbookData = (New-Object System.Net.WebClient).DownloadString($workbookFinalPath) $dependencies = $workbookData | ConvertFrom-Json | Where-Object { $indexOfOpenBraces = $_.templateRelativePath.contains('(') if ($indexOfOpenBraces) { $lastIndexOfDot = $_.templateRelativePath.LastIndexOf('.') if($lastIndexOfDot -gt 0) { $value = $_.templateRelativePath.substring(0, $lastIndexOfDot) if ($value.ToLower() -eq $workbookKey.ToLower()) { return $_; } } } else { if($_.templateRelativePath.split('.')[0].ToLower() -eq $workbookKey.ToLower()) { return $_; } } } if ($dependencies.Count -gt 0) { $dependencies = $dependencies[0] } $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"; #criteria = $WorkbookDependencyCriteria; }; } 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" }; } $global:baseMainTemplate.variables | Add-Member -NotePropertyName "workbookVersion$global:workbookCounter" -NotePropertyValue "$($dependencies.version)" $global:baseMainTemplate.variables | Add-Member -NotePropertyName "workbookContentId$global:workbookCounter" -NotePropertyValue "$($dependencies.workbookKey)" $global:baseMainTemplate.parameters | Add-Member -MemberType NoteProperty -Name $workbookNameParameterName -Value $workbookNameParameter $global:baseMainTemplate.variables | Add-Member -NotePropertyName "workbookId$global:workbookCounter" -NotePropertyValue "[resourceId('Microsoft.Insights/workbooks', variables('workbookContentId$global:workbookCounter'))]" if ($contentResourceDetails.apiVersion -eq '3.0.0') { $global:baseMainTemplate.variables | Add-Member -NotePropertyName "workbookTemplateSpecName$global:workbookCounter" -NotePropertyValue "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-wb-',uniquestring(variables('_workbookContentId$global:workbookCounter'))))]" } else { $global:baseMainTemplate.variables | Add-Member -NotePropertyName "workbookTemplateSpecName$global:workbookCounter" -NotePropertyValue "[concat(parameters('workspace'),'-wb-',uniquestring(variables('_workbookContentId$global:workbookCounter')))]" } $global:baseMainTemplate.variables | Add-Member -NotePropertyName "_workbookContentId$global:workbookCounter" -NotePropertyValue "[variables('workbookContentId$global:workbookCounter')]" $global:DependencyCriteria += [PSCustomObject]@{ kind = "Workbook"; contentId = "[variables('_workbookContentId$global:workbookCounter')]"; version = "[variables('workbookVersion$global:workbookCounter')]"; }; # Add workspace resource ID if not available if (!$global:baseMainTemplate.variables.workspaceResourceId) { $global:baseMainTemplate.variables | Add-Member -NotePropertyName "workspaceResourceId" -NotePropertyValue "[resourceId('microsoft.OperationalInsights/Workspaces', parameters('workspace'))]" } if ($contentResourceDetails.contentSchemaVersion -ne '3.0.0') { # Add base templateSpec $baseWorkbookTemplateSpec = [PSCustomObject]@{ type = $contentResourceDetails.resourcetype; # "Microsoft.Resources/templateSpecs"; apiVersion = $contentResourceDetails.templateSpecsApiVersion; # "2022-02-01"; name = "[variables('workbookTemplateSpecName$global: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"; } } $global:baseMainTemplate.resources += $baseWorkbookTemplateSpec } $newWorkbook.name = "[variables('workbookContentId$global:workbookCounter')]" $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 = $contentResourceDetails.commonResourceMetadataApiVersion; #"2022-01-01-preview"; name = "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('Workbook-', last(split(variables('workbookId$global:workbookCounter'),'/'))))]"; properties = [PSCustomObject]@{ description = "$dependencies.description"; parentId = "[variables('workbookId$global:workbookCounter')]" contentId = "[variables('_workbookContentId$global:workbookCounter')]"; kind = "Workbook"; version = "[variables('workbookVersion$global:workbookCounter')]"; source = [PSCustomObject]@{ kind = "Solution"; name = $contentToImport.Name; sourceId = "[variables('_solutionId')]" }; author = $authorDetails; support = $baseMetadata.support; #dependencies = $workbookDependencies; } } 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 = $contentResourceDetails.subtype; # "Microsoft.Resources/templateSpecs/versions"; apiVersion = $contentResourceDetails.templateSpecsVersionApiVersion; name = $contentResourceDetails.apiVersion -eq '3.0.0' ? "[variables('workbookTemplateSpecName$global:workbookCounter')]" : "[concat(variables('workbookTemplateSpecName$global:workbookCounter'),'/',variables('workbookVersion$global:workbookCounter'))]"; location = "[parameters('workspace-location')]"; tags = [PSCustomObject]@{ "hidden-sentinelWorkspaceId" = "[variables('workspaceResourceId')]"; "hidden-sentinelContentType" = "Workbook"; }; dependsOn = $contentResourceDetails.apiVersion -eq '3.0.0' ? @( "$($contentResourceDetails.dependsOn)") : @( "$($contentResourceDetails.dependsOn), variables('workbookTemplateSpecName$global:workbookCounter'))]" ); properties = [PSCustomObject]@{ description = "$($workbookKey) Workbook with template version $($contentToImport.Version)"; mainTemplate = [PSCustomObject]@{ '$schema' = "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#"; contentVersion = "[variables('workbookVersion$global:workbookCounter')]"; parameters = [PSCustomObject]@{}; variables = [PSCustomObject]@{}; resources = @( # workbook $newWorkbook, # Metadata $workbookMetadata ) }; } } if ($contentResourceDetails.apiVersion -eq '3.0.0') { $workbookTemplateSpecContent = SetContentTemplateDefaultValuesToProperties -templateSpecResourceObj $workbookTemplateSpecContent $workbookTemplateSpecContent.properties.contentId = "[variables('_workbookContentId$global:workbookCounter')]" $workbookTemplateSpecContent.properties.contentKind = "Workbook" $workbookTemplateSpecContent.properties.displayName = "[parameters('workbook$global:workbookCounter-name')]" $global:baseMainTemplate.variables | Add-Member -NotePropertyName "_workbookcontentProductId$global:workbookCounter" -NotePropertyValue "[concat(take(variables('_solutionId'),50),'-','wb','-', uniqueString(concat(variables('_solutionId'),'-','Workbook','-',variables('_workbookContentId$global:workbookCounter'),'-', variables('workbookVersion$global:workbookCounter'))))]" $workbookTemplateSpecContent.properties.contentProductId = "[variables('_workbookcontentProductId$global:workbookCounter')]" $workbookTemplateSpecContent.properties.id = "[variables('_workbookcontentProductId$global:workbookCounter')]" $workbookTemplateSpecContent.properties.version = "[variables('workbookVersion$global:workbookCounter')]" $workbookTemplateSpecContent.PSObject.Properties.Remove('tags') } $global:baseMainTemplate.resources += $workbookTemplateSpecContent } else { $major = $contentToImport.version.split(".")[0] #if ($contentToImport.version -ne '3.0.0' ) if ($major -ne 3) { $global:baseMainTemplate.resources += $newWorkbook if ($contentToImport.Metadata -or $isPipelineRun) { $global:baseMainTemplate.variables | Add-Member -NotePropertyName $fileName -NotePropertyValue $fileName $global:baseMainTemplate.variables | Add-Member -NotePropertyName "_$fileName" -NotePropertyValue "[variables('$fileName')]" $global:DependencyCriteria += [PSCustomObject]@{ kind = "Workbook"; contentId = "[variables('_$fileName')]"; version = "[variables('workbookVersion$global:workbookCounter')]"; }; } } } $global:workbookCounter += 1 } function GetPlaybookDataMetadata($file, $contentToImport, $contentResourceDetails, $json, $isPipelineRun) { 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 -or $isPipelineRun) { $global:baseMainTemplate.variables | Add-Member -NotePropertyName $fileName -NotePropertyValue $fileName $global: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; $global:DependencyCriteria += [PSCustomObject]@{ kind = $IsLogicAppsCustomConnector ? "LogicAppsCustomConnector" : $IsFunctionAppResource ? "AzureFunction" : "Playbook"; contentId = "[variables('_$fileName')]"; version = "[variables('playbookVersion$global:playbookCounter')]"; }; if($IsFunctionAppResource) { $functionAppsPlaybookId = $playbookData.parameters.FunctionAppName.defaultValue # keeping this for furture use # if ($null -eq $functionAppsPlaybookId) # { # $suffix = 'fa' # if (!$playbookName) { # $functionAppMessage = "FunctionAppname not found in FunctionApp file $fileName so setting default value to '" + $fileName + "fa'" # Write-Host "$functionAppMessage" # $functionAppsPlaybookId = $fileName + $suffix # } # else { # $functionAppMessage = "FunctionAppname not found in FunctionApp file $fileName so setting default value to '" + $playbookName + "fa'" # Write-Host "$functionAppMessage" # $functionAppsPlaybookId = $playbookName + $suffix # } # } } if (!$playbookName) { $playbookName = $fileName; } if ($global: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" } } }) } $global:baseCreateUiDefinition.parameters.steps += $playbookStep } $playbookDescriptionText = $(if ($contentToImport.PlaybookDescription -and $contentToImport.PlaybookDescription -is [System.Array]) { $contentToImport.PlaybookDescription[$global:playbookCounter - 1] } elseif ($contentToImport.PlaybookDescription -and $contentToImport.PlaybookDescription -is [System.String]) { $contentToImport.PlaybookDescription } else { "" }) $playbookElement = [PSCustomObject] @{ name = "playbook$global:playbookCounter"; type = "Microsoft.Common.Section"; label = $playbookName; elements = @( [PSCustomObject] @{ name = "playbook$global: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 = $global:baseCreateUiDefinition.parameters.steps.Count - 1 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$global: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" } } if(!$contentToImport.TemplateSpec) { $global:baseMainTemplate.parameters | Add-Member -NotePropertyName "playbook$global: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$global: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"; } } if(!$contentToImport.TemplateSpec){ $global:baseMainTemplate.parameters | Add-Member -NotePropertyName "playbook$global: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$global: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; }; } if(!$contentToImport.TemplateSpec){ $global:baseMainTemplate.parameters | Add-Member -NotePropertyName "playbook$global: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$global: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; }; } if(!$contentToImport.TemplateSpec) { $global:baseMainTemplate.parameters | Add-Member -NotePropertyName "playbook$global: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$global: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$global: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)" } } } ) $defaultValue = $(if ($defaultParamValue) { $defaultParamValue } else { "" }) if(!$contentToImport.TemplateSpec){ $global:baseMainTemplate.parameters | Add-Member -NotePropertyName "playbook$global:playbookCounter-$paramName" -NotePropertyValue ([PSCustomObject] @{ defaultValue = $defaultValue; type = "string"; minLength = 1; }) } } if(!$contentToImport.TemplateSpec){ $global:baseCreateUiDefinition.parameters.outputs | Add-Member -NotePropertyName "playbook$global:playbookCounter-$paramName" -NotePropertyValue "[steps('playbooks').playbook$global:playbookCounter.playbook$global: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 $global:playbookCounter) } if($contentToImport.TemplateSpec -and $variableName.ToLower().Contains("connection")) { $variableValue = "[" + $variableValue ; } if (!$contentToImport.TemplateSpec -and $variableName.ToLower().contains("apikey")) { $global:baseMainTemplate.variables | Add-Member -NotePropertyName "playbook-$variableName" -NotePropertyValue "[$variableValue]" } elseif (!$contentToImport.TemplateSpec) { $global:baseMainTemplate.variables | Add-Member -NotePropertyName "playbook$global: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]) { if($resourceObj.$key -eq "") { $resourceObj.$key = "[variables('blanks')]"; if (!$global:baseMainTemplate.variables.blanks) { $global: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')]") -and $prop.Value -ne "[variables('TemplateEmptyObject')]" -and $prop.Value -ne "[variables('TemplateNullVariable')]") { $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])) { if ($prop.Value -like ('*hidden-link*')) { $jsonValue = $prop.Value | ConvertTo-Json $jsonValue = $jsonValue.replace('concat', '[concat') $prop.Value = $jsonValue | ConvertFrom-Json } $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)" $global: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)" $global:playbookCounter) } $resourceObj.$key = $(node "$PSScriptRoot/templating/replaceLocationValue.js" "$(replaceQuotes $resourceObj.$key)" $global:playbookCounter) if ($resourceObj.$key.IndexOf($azureManagementUrl)) { $resourceObj.$key = $resourceObj.$key.Replace($azureManagementUrl, "@{variables('azureManagementUrl')}") $azureManagementUrlExists = $true } if ($key -eq "operationId") { $global:baseMainTemplate.variables | Add-Member -NotePropertyName "operationId-$($resourceobj.$key)" -NotePropertyValue $($resourceobj.$key) $global: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 $global:playbookCounter) $item = $(node "$PSScriptRoot/templating/replacePlaybookParamNames.js" $item $global: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 $global: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 $($global:baseMainTemplate.variables).PSObject.Properties) { if ($templateVar.Value -eq $connectionVariable) { return $templateVar.Name } } return $false } $playbookDependencies = @(); $playbookResources = @(); $playbookVersion = '1.0'; $logicAppsPlaybookId = ''; $global:contentShortName = ''; $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 = $global:customConnectorsList[$templateVar.Value.defaultValue].id; version = $global: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 $global: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 } $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 $global:functionAppList.ContainsKey($param.Value.defaultValue)) { $playbookDependencies += [PSCustomObject] @{ kind = "AzureFunction"; contentId = $global:functionAppList[$param.Value.defaultValue].id; version = $global:functionAppList[$param.Value.defaultValue].version; } } } } } if($contentToImport.TemplateSpec) { $global:baseMainTemplate.variables | Add-Member -NotePropertyName "playbookVersion$global:playbookCounter" -NotePropertyValue $playbookVersion $global:baseMainTemplate.variables | Add-Member -NotePropertyName "playbookContentId$global:playbookCounter" -NotePropertyValue $fileName $global:baseMainTemplate.variables | Add-Member -NotePropertyName "_playbookContentId$global:playbookCounter" -NotePropertyValue "[variables('playbookContentId$global:playbookCounter')]" if (!$IsLogicAppsCustomConnector -and !$IsFunctionAppResource) { $global:baseMainTemplate.variables | Add-Member -NotePropertyName "playbookId$global:playbookCounter" -NotePropertyValue "[resourceId('Microsoft.Logic/workflows', variables('playbookContentId$global:playbookCounter'))]" } if ($contentResourceDetails.apiVersion -eq '3.0.0') { if ($IsLogicAppsCustomConnector) { $global:baseMainTemplate.variables | Add-Member -NotePropertyName "playbookTemplateSpecName$global:playbookCounter" -NotePropertyValue "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-lc-',uniquestring(variables('_playbookContentId$global:playbookCounter'))))]" $global:contentShortName = 'lc' } elseif ($IsFunctionAppResource) { $global:baseMainTemplate.variables | Add-Member -NotePropertyName "playbookTemplateSpecName$global:playbookCounter" -NotePropertyValue "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-fa-',uniquestring(variables('_playbookContentId$global:playbookCounter'))))]" $global:contentShortName = 'fa' } else { $global:baseMainTemplate.variables | Add-Member -NotePropertyName "playbookTemplateSpecName$global:playbookCounter" -NotePropertyValue "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-pl-',uniquestring(variables('_playbookContentId$global:playbookCounter'))))]" $global:contentShortName = 'pl' } } else { if ($IsLogicAppsCustomConnector) { $global:baseMainTemplate.variables | Add-Member -NotePropertyName "playbookTemplateSpecName$global:playbookCounter" -NotePropertyValue "[concat(parameters('workspace'),'-lc-',uniquestring(variables('_playbookContentId$global:playbookCounter')))]" $global:contentShortName = 'lc' } elseif ($IsFunctionAppResource) { $global:baseMainTemplate.variables | Add-Member -NotePropertyName "playbookTemplateSpecName$global:playbookCounter" -NotePropertyValue "[concat(parameters('workspace'),'-fa-',uniquestring(variables('_playbookContentId$global:playbookCounter')))]" $global:contentShortName = 'fa' } else { $global:baseMainTemplate.variables | Add-Member -NotePropertyName "playbookTemplateSpecName$global:playbookCounter" -NotePropertyValue "[concat(parameters('workspace'),'-pl-',uniquestring(variables('_playbookContentId$global:playbookCounter')))]" $global:contentShortName = 'pl' } } # Add workspace resource ID if not available if (!$global:baseMainTemplate.variables.workspaceResourceId) { $global:baseMainTemplate.variables | Add-Member -NotePropertyName "workspaceResourceId" -NotePropertyValue "[resourceId('microsoft.OperationalInsights/Workspaces', parameters('workspace'))]" } # Add base templateSpec if ($contentResourceDetails.contentSchemaVersion -ne '3.0.0') { $basePlaybookTemplateSpec = [PSCustomObject]@{ type = $contentResourceDetails.resourcetype; # "Microsoft.Resources/templateSpecs"; apiVersion = $contentResourceDetails.templateSpecsApiVersion; #"2022-02-01"; name = "[variables('playbookTemplateSpecName$global: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"; } } $global: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) { $global:customConnectorsList.add($customConnectorContentId, @{ id="[variables('_$filename')]"; version="[variables('playbookVersion$global:playbookCounter')]"}); } if ($IsFunctionAppResource) { $global:functionAppList.add($functionAppsPlaybookId, @{ id="[variables('_$filename')]"; version="[variables('playbookVersion$global:playbookCounter')]"}); # keeping it for furture use # if ($null -ne $functionAppsPlaybookId) # { # $global:functionAppList.add($functionAppsPlaybookId, @{ id="[variables('_$filename')]"; version="[variables('playbookVersion$global:playbookCounter')]"}); # } } $playbookMetadata = [PSCustomObject]@{ type = "Microsoft.OperationalInsights/workspaces/providers/metadata"; apiVersion = $contentResourceDetails.commonResourceMetadataApiVersion; #"2022-01-01-preview"; name = $IsLogicAppsCustomConnector ? "[[concat(variables('workspace-name'),'/Microsoft.SecurityInsights/',concat('LogicAppsCustomConnector-', last(split(variables('playbookId$global:playbookCounter'),'/'))))]" : $IsFunctionAppResource ? "[[concat(variables('workspace-name'),'/Microsoft.SecurityInsights/',concat('AzureFunction-', last(split(variables('playbookId$global:playbookCounter'),'/'))))]" : "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('Playbook-', last(split(variables('playbookId$global:playbookCounter'),'/'))))]"; properties = [PSCustomObject]@{ parentId = $IsLogicAppsCustomConnector -or $IsFunctionAppResource ? "[[variables('playbookId$global:playbookCounter')]" : "[variables('playbookId$global:playbookCounter')]" contentId = "[variables('_playbookContentId$global:playbookCounter')]"; kind = $IsLogicAppsCustomConnector ? "LogicAppsCustomConnector" : $IsFunctionAppResource ? "AzureFunction" : "Playbook"; version = "[variables('playbookVersion$global: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$global:playbookCounter" -NotePropertyValue $fileName; $playbookVariables | Add-Member -NotePropertyName "playbookId$global:playbookCounter" -NotePropertyValue "[[resourceId('Microsoft.Web/customApis', parameters('$logicAppsPlaybookId'))]" } if ($IsFunctionAppResource) { $playbookVariables | Add-Member -NotePropertyName "playbookContentId$global:playbookCounter" -NotePropertyValue $fileName; $playbookVariables | Add-Member -NotePropertyName "playbookId$global:playbookCounter" -NotePropertyValue "[[resourceId('Microsoft.Logic/workflows', variables('playbookContentId$global: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 = $contentResourceDetails.subtype; # "Microsoft.Resources/templateSpecs/versions"; apiVersion = $contentResourceDetails.templateSpecsVersionApiVersion; name = $contentResourceDetails.apiVersion -eq '3.0.0' ? "[variables('playbookTemplateSpecName$global:playbookCounter')]" : "[concat(variables('playbookTemplateSpecName$global:playbookCounter'),'/',variables('playbookVersion$global:playbookCounter'))]"; location = "[parameters('workspace-location')]"; tags = [PSCustomObject]@{ "hidden-sentinelWorkspaceId" = "[variables('workspaceResourceId')]"; "hidden-sentinelContentType" = $IsLogicAppsCustomConnector ? "LogicAppsCustomConnector" : $IsFunctionAppResource ? "AzureFunction" : "Playbook"; }; dependsOn = $contentResourceDetails.apiVersion -eq '3.0.0' ? @( "$($contentResourceDetails.dependsOn)") : @( "$($contentResourceDetails.dependsOn), variables('playbookTemplateSpecName$global: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$global:playbookCounter')]"; parameters = $playbookData.parameters; variables = $playbookVariables; resources = $playbookResources; }; } } if ($contentResourceDetails.apiVersion -eq '3.0.0') { $playbookTemplateSpecContent = SetContentTemplateDefaultValuesToProperties -templateSpecResourceObj $playbookTemplateSpecContent $playbookTemplateSpecContent.properties.contentId = "[variables('_playbookContentId$global:playbookCounter')]" $playbookTemplateSpecContent.properties.contentKind = $IsLogicAppsCustomConnector ? "LogicAppsCustomConnector" : $IsFunctionAppResource ? "AzureFunction" : "Playbook" $playbookTemplateSpecContent.properties.displayName = $playbookName $global:baseMainTemplate.variables | Add-Member -NotePropertyName "_playbookcontentProductId$global:playbookCounter" -NotePropertyValue "[concat(take(variables('_solutionId'),50),'-','$global:contentShortName','-', uniqueString(concat(variables('_solutionId'),'-','$($IsLogicAppsCustomConnector ? "LogicAppsCustomConnector" : $IsFunctionAppResource ? "AzureFunction" : "Playbook")','-',variables('_playbookContentId$global:playbookCounter'),'-', variables('playbookVersion$global:playbookCounter'))))]" $playbookTemplateSpecContent.properties.contentProductId = "[variables('_playbookcontentProductId$global:playbookCounter')]" $playbookTemplateSpecContent.properties.id = "[variables('_playbookcontentProductId$global:playbookCounter')]" $playbookTemplateSpecContent.properties.version = "[variables('playbookVersion$global:playbookCounter')]" $playbookTemplateSpecContent.PSObject.Properties.Remove('tags') } 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 (!$global:baseMainTemplate.variables.blanks) { $global: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"); } $global:baseMainTemplate.resources += $playbookTemplateSpecContent; } else { $global:baseMainTemplate.resources += $playbookResources; } if ($azureManagementUrlExists) { $global:baseMainTemplate.variables | Add-Member -NotePropertyName "azureManagementUrl" -NotePropertyValue $azureManagementUrl } $global:playbookCounter += 1 } $global:ccpCounter = 1 function GetDataConnectorMetadata($file, $contentResourceDetails, $dataFileMetadata, $solutionFileMetadata, $dcFolderName, $ccpDict = $null, $solutionBasePath, $solutionName, $ccpTables, $ccpTablesCounter) { Write-Host "Generating Data Connector using $file" if ($null -ne $ccpDict) { # execute ccp integration code which is using CLV2 if ($global:ccpCounter -eq 1) { . "$PSScriptRoot/createCCPConnector.ps1" # load ccp resource creator createCCPConnectorResources -contentResourceDetails $contentResourceDetails -dataFileMetadata $dataFileMetadata -solutionFileMetadata $solutionFileMetadata -dcFolderName $dcFolderName -ccpDict $ccpDict -solutionBasePath $solutionBasePath -solutionName $solutionName -ccpTables $ccpTables -ccpTablesCounter $ccpTablesCounter $global:ccpCounter += 1 } return } try { # BELOW IS OLD WAY OF CCP CONNECTOR CODE I.E CLV1 WHICH ONLY HAS CONNECTORUICONFIG AND POLLER CONFIG SECTION IN SOURCE $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; } $hasStaticDataConnectorIdsProperty = [bool]($contentToImport.PSObject.Properties.Name.tolower() -match 'staticdataconnectorids') ? $true : $false $is1PConnector = $false if ($hasStaticDataConnectorIdsProperty) { $staticDataConnectorIdsArray = $contentToImport.StaticDataConnectorIds if ($null -ne $staticDataConnectorIdsArray -and $staticDataConnectorIdsArray.count -gt 0) { $dataConnectorId = $connectorData.id $hasDataConnectorId = [bool]($contentToImport.StaticDataConnectorIds | Where-Object { $_ -eq "$dataConnectorId"} ) if ($hasDataConnectorId) { # data connector id is specified in GenericDataConnectorIds array so we have to make this as generic connector $is1PConnector = $true } } } $global:DependencyCriteria += [PSCustomObject]@{ kind = "DataConnector"; contentId = if ($contentToImport.TemplateSpec){"[variables('_dataConnectorContentId$global:connectorCounter')]"}else{"[variables('_$connectorId')]"}; version = if ($contentToImport.TemplateSpec){"[variables('dataConnectorVersion$global: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 (!$global:baseMainTemplate.variables.workspaceResourceId -and $contentResourceDetails.contentSchemaVersion -ne '3.0.0') { $global: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) { $global:baseMainTemplate.variables | Add-Member -NotePropertyName "uiConfigId$global:connectorCounter" -NotePropertyValue $templateSpecConnectorData.id $global:baseMainTemplate.variables | Add-Member -NotePropertyName "_uiConfigId$global:connectorCounter" -NotePropertyValue "[variables('uiConfigId$global:connectorCounter')]" } $global:baseMainTemplate.variables | Add-Member -NotePropertyName "dataConnectorContentId$global:connectorCounter" -NotePropertyValue $templateSpecConnectorData.id $global:baseMainTemplate.variables | Add-Member -NotePropertyName "_dataConnectorContentId$global:connectorCounter" -NotePropertyValue "[variables('dataConnectorContentId$global:connectorCounter')]" $global:baseMainTemplate.variables | Add-Member -NotePropertyName "dataConnectorId$global:connectorCounter" -NotePropertyValue "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/dataConnectors', variables('_dataConnectorContentId$global:connectorCounter'))]" $global:baseMainTemplate.variables | Add-Member -NotePropertyName "_dataConnectorId$global:connectorCounter" -NotePropertyValue "[variables('dataConnectorId$global:connectorCounter')]" if ($contentResourceDetails.apiVersion -eq '3.0.0') { $global:baseMainTemplate.variables | Add-Member -NotePropertyName "dataConnectorTemplateSpecName$global:connectorCounter" -NotePropertyValue "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-dc-',uniquestring(variables('_dataConnectorContentId$global:connectorCounter'))))]" } else { $global:baseMainTemplate.variables | Add-Member -NotePropertyName "dataConnectorTemplateSpecName$global:connectorCounter" -NotePropertyValue "[concat(parameters('workspace'),'-dc-',uniquestring(variables('_dataConnectorContentId$global:connectorCounter')))]" } $global:baseMainTemplate.variables | Add-Member -NotePropertyName "dataConnectorVersion$global:connectorCounter" -NotePropertyValue (($null -ne $templateSpecConnectorData.metadata) ? "$($templateSpecConnectorData.metadata.version)" : "1.0.0") if (!$contentToImport.TemplateSpec){ $global:baseMainTemplate.variables | Add-Member -NotePropertyName "parentId" -NotePropertyValue $global:solutionId $global:baseMainTemplate.variables | Add-Member -NotePropertyName "_parentId" -NotePropertyValue "[variables('parentId')]" }; if ($contentResourceDetails.contentSchemaVersion -ne '3.0.0') { # Add base templateSpec $baseDataConnectorTemplateSpec = [PSCustomObject]@{ type = $contentResourceDetails.resourcetype; # "Microsoft.Resources/templateSpecs"; apiVersion = $contentResourceDetails.templateSpecsApiVersion; #"2022-02-01"; name = "[variables('dataConnectorTemplateSpecName$global: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"; } } $global:baseMainTemplate.resources += $baseDataConnectorTemplateSpec } if(!$is1PConnector) { $existingFunctionApp = $false; $instructionArray = $templateSpecConnectorData.instructionSteps ($instructionArray | ForEach {if($_.description -and $_.description.IndexOf('[Deploy To Azure]') -gt 0){$existingFunctionApp = $true;}}) $hasFunctionAppManualDeploymentText = $instructionArray | Where-Object { $_.title -and $_.title.IndexOf('Manual Deployment of Azure Functions') -gt 0 } 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) { $hasFunctionAppManualDeploymentText = $false 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 -and $desc.description.IndexOf('Deploy To Azure') -gt 0) { $existingFunctionApp = $true break } } foreach ($desc in $instructionItem.parameters.instructionSteps) { if ($desc.title -and $desc.title.IndexOf('Manual Deployment of Azure Functions') -gt 0) { $hasFunctionAppManualDeploymentText = $true break } } } } } } } if($existingFunctionApp) { $templateSpecConnectorData.title = ($templateSpecConnectorData.title.Contains("using Azure Functions")) ? $templateSpecConnectorData.title : $hasFunctionAppManualDeploymentText ? $templateSpecConnectorData.title + " (using Azure Functions)" : $templateSpecConnectorData.title; } } # Data Connector Content -- *Assumes GenericUI if($is1PConnector) { $1pconnectorData = $templateSpecConnectorData $1pconnectorData = $1pconnectorData | Select-Object -Property id,title,publisher,descriptionMarkdown, graphQueries, connectivityCriterias,dataTypes } $templateSpecConnectorUiConfig = ($is1PConnector -eq $true) ? $1pconnectorData : $templateSpecConnectorData $templateSpecConnectorUiConfig.id = "[variables('_uiConfigId$global:connectorCounter')]" if($is1PConnector -eq $false) { $hasIsPreviewProperty = [bool]($templateSpecConnectorUiConfig.availability.PsObject.Properties.Name.tolower() -match "ispreview") if (!$hasIsPreviewProperty) { $templateSpecConnectorUiConfig.availability | Add-Member -NotePropertyName "isPreview" -NotePropertyValue $false } else { $templateSpecConnectorUiConfig.availability.isPreview = ($templateSpecConnectorUiConfig.availability.isPreview -eq $true) ? $false : $templateSpecConnectorUiConfig.availability.isPreview } } $dataConnectorContent = [PSCustomObject]@{ name = "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',variables('_dataConnectorContentId$global:connectorCounter'))]"; apiVersion = $contentResourceDetails.dataConnectorsApiVersion; #"2021-03-01-preview"; type = "Microsoft.OperationalInsights/workspaces/providers/dataConnectors"; location = "[parameters('workspace-location')]"; kind = ($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 = $contentResourceDetails.metadataApiVersion; #"2022-01-01-preview"; name = "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('DataConnector-', last(split(variables('_dataConnectorId$global:connectorCounter'),'/'))))]"; properties = [PSCustomObject]@{ parentId = "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/dataConnectors', variables('_dataConnectorContentId$global:connectorCounter'))]"; contentId = "[variables('_dataConnectorContentId$global:connectorCounter')]"; kind = "DataConnector"; version = "[variables('dataConnectorVersion$global:connectorCounter')]"; source = [PSCustomObject]@{ kind = "Solution"; name = $contentToImport.Name; sourceId = "[variables('_solutionId')]" }; author = $authorDetails; support = $baseMetadata.support } } #one removed check from here for apiVersion # Add templateSpecs/versions resource to hold actual content $dataConnectorTemplateSpecContent = [PSCustomObject]@{ type = $contentResourceDetails.subtype; # "Microsoft.Resources/templateSpecs/versions"; apiVersion = $contentResourceDetails.templateSpecsVersionApiVersion; name = $contentResourceDetails.apiVersion -eq '3.0.0' ? "[variables('dataConnectorTemplateSpecName$global:connectorCounter')]" : "[concat(variables('dataConnectorTemplateSpecName$global:connectorCounter'),'/',variables('dataConnectorVersion$global:connectorCounter'))]"; location = "[parameters('workspace-location')]"; tags = [PSCustomObject]@{ "hidden-sentinelWorkspaceId" = "[variables('workspaceResourceId')]"; "hidden-sentinelContentType" = "DataConnector"; }; dependsOn = $contentResourceDetails.apiVersion -eq '3.0.0' ? @( "$($contentResourceDetails.dependsOn)") : @( "$($contentResourceDetails.dependsOn), variables('dataConnectorTemplateSpecName$global: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$global:connectorCounter')]"; parameters = [PSCustomObject]@{}; variables = [PSCustomObject]@{}; resources = @( # Data Connector $dataConnectorContent, # Metadata $dataConnectorMetadata ) }; } } if ($contentResourceDetails.apiVersion -eq '3.0.0') { $dataConnectorTemplateSpecContent = SetContentTemplateDefaultValuesToProperties -templateSpecResourceObj $dataConnectorTemplateSpecContent $dataConnectorTemplateSpecContent.properties.contentId = "[variables('_dataConnectorContentId$global:connectorCounter')]" $dataConnectorTemplateSpecContent.properties.contentKind = "DataConnector" $global:baseMainTemplate.variables | Add-Member -NotePropertyName "_dataConnectorcontentProductId$global:connectorCounter" -NotePropertyValue "[concat(take(variables('_solutionId'),50),'-','dc','-', uniqueString(concat(variables('_solutionId'),'-','DataConnector','-',variables('_dataConnectorContentId$global:connectorCounter'),'-', variables('dataConnectorVersion$global:connectorCounter'))))]" $dataConnectorTemplateSpecContent.properties.contentProductId = "[variables('_dataConnectorcontentProductId$global:connectorCounter')]" $dataConnectorTemplateSpecContent.properties.id = "[variables('_dataConnectorcontentProductId$global:connectorCounter')]" $dataConnectorTemplateSpecContent.properties.displayName = $templateSpecConnectorData.title $dataConnectorTemplateSpecContent.properties.version = "[variables('dataConnectorVersion$global:connectorCounter')]" $dataConnectorTemplateSpecContent.PSObject.Properties.Remove('tags') } $global:baseMainTemplate.resources += $dataConnectorTemplateSpecContent # Add content-metadata item, in addition to template spec metadata item $dataConnectorActiveContentMetadata = [PSCustomObject]@{ type = "Microsoft.OperationalInsights/workspaces/providers/metadata"; apiVersion = $contentResourceDetails.metadataApiVersion; #"2022-01-01-preview"; name = "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('DataConnector-', last(split(variables('_dataConnectorId$global:connectorCounter'),'/'))))]"; dependsOn = @("[variables('_dataConnectorId$global:connectorCounter')]"); location = "[parameters('workspace-location')]"; properties = [PSCustomObject]@{ parentId = "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/dataConnectors', variables('_dataConnectorContentId$global:connectorCounter'))]"; contentId = "[variables('_dataConnectorContentId$global:connectorCounter')]"; kind = "DataConnector"; version = "[variables('dataConnectorVersion$global:connectorCounter')]"; source = [PSCustomObject]@{ kind = "Solution"; name = $contentToImport.Name; sourceId = "[variables('_solutionId')]" }; author = $authorDetails; support = $baseMetadata.support } } $global: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(!$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$global:connectorCounter')]" } $connectorObj = [PSCustomObject]@{ name = if ($contentToImport.TemplateSpec) { "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',variables('_dataConnectorContentId$global:connectorCounter'))]" }else { "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',parameters('connector$global:connectorCounter-name'))]" } apiVersion = $contentResourceDetails.dataConnectorsApiVersion; #"2021-03-01-preview"; type = "Microsoft.OperationalInsights/workspaces/providers/dataConnectors"; location = "[parameters('workspace-location')]"; kind = ($is1PConnector -eq $true) ? "StaticUI" : "GenericUI"; properties = [PSCustomObject]@{ connectorUiConfig = $standardConnectorUiConfig } } if(!$contentToImport.TemplateSpec) { $connectorObj | Add-Member -NotePropertyName "id" -NotePropertyValue "[variables('_connector$global: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.id = if ($contentToImport.TemplateSpec) { "[variables('_uiConfigId$global:connectorCounter')]" }else { "[variables('_connector$global:connectorCounter-source')]" }; $connectorObj = [PSCustomObject]@{ name = if ($contentToImport.TemplateSpec) { "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',variables('_dataConnectorContentId$global:connectorCounter'))]" }else { "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',parameters('connector$global:connectorCounter-name'))]" } apiVersion = $contentResourceDetails.dataConnectorsApiVersion; #"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 } $global:baseMainTemplate.resources += $connectorObj $syslog = "Syslog" $commonSecurityLog = "CommonSecurityLog" function getConnectorDataTypes($dataTypesArray) { $typeResult = "custom log" foreach ($dataType in $dataTypesArray) { if ($dataType.name.IndexOf($syslog) -ne -1) { $typeResult = $syslog } elseif ($dataType.name.IndexOf($commonSecurityLog) -ne -1) { $typeResult = $commonSecurityLog } } return $typeResult } function getAllDataTypeNames($dataTypesArray) { $typeResult = @() foreach ($dataType in $dataTypesArray) { $typeResult += $dataType.name } return $typeResult } $connectorDataType = $(getConnectorDataTypes $connectorData.dataTypes) $isParserAvailable = $($contentToImport.Parsers -and ($contentToImport.Parsers.Count -gt 0)) $baseDescriptionText = "This Solution installs the data connector for $solutionName. You can get $solutionName $connectorDataType data in your Microsoft Sentinel workspace. Configure and enable this data connector in the Data Connector gallery after this Solution deploys." $customLogsText = "$baseDescriptionText This data connector creates custom log table(s) $(getAllDataTypeNames $connectorData.dataTypes) in your Microsoft Sentinel / Azure Log Analytics workspace." $syslogText = "$baseDescriptionText The logs will be received in the Syslog table in your Microsoft Sentinel / Azure Log Analytics workspace." $commonSecurityLogText = "$baseDescriptionText The logs will be received in the CommonSecurityLog table in your Microsoft Sentinel / Azure Log Analytics workspace." $connectorDescriptionText = "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$global:connectorCounter-text"; type = "Microsoft.Common.TextBlock"; options = [PSCustomObject] @{ text = $connectorDescriptionText; } } if ($global:connectorCounter -eq 1) { $global:baseCreateUiDefinition.parameters.steps += $baseDataConnectorStep } $currentStepNum = $global:baseCreateUiDefinition.parameters.steps.Count - 1 $global:baseCreateUiDefinition.parameters.steps[$currentStepNum].elements += $baseDataConnectorTextElement if ($global:connectorCounter -eq $contentToImport."Data Connectors".Count) { $parserTextElement = [PSCustomObject] @{ name = "dataconnectors-parser-text"; type = "Microsoft.Common.TextBlock"; options = [PSCustomObject] @{ text = $parserText; } } $connectDataSourcesLink = [PSCustomObject] @{ name = "dataconnectors-link$($global:connectorCounter)"; 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) { $global:baseCreateUiDefinition.parameters.steps[$currentStepNum].elements += $parserTextElement } $global:baseCreateUiDefinition.parameters.steps[$currentStepNum].elements += $connectDataSourcesLink } # Update Connector Counter $global:connectorCounter += 1 } function GetHuntingDataMetadata($file, $rawData, $contentResourceDetails) { 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"; $hasHuntingQueryVersion = [bool]($global:baseMainTemplate.variables.PSobject.Properties.Name -match "variables('huntingQueryObject$global:huntingQueryCounter').huntingQueryVersion$global:huntingQueryCounter") if ($hasHuntingQueryVersion) { $global:huntingQueryCounter += 1 } $calculatedHuntingQueryVersion = ($null -ne $yaml.version) ? "$($yaml.version)" : "1.0.0" $objHuntingQueryVariables = [pscustomobject]@{} $objHuntingQueryVariables | Add-Member -NotePropertyName "huntingQueryVersion$global:huntingQueryCounter" -NotePropertyValue "$calculatedHuntingQueryVersion" $huntingQueryContentIdValue = "$($yaml.id)" $objHuntingQueryVariables | Add-Member -NotePropertyName "_huntingQuerycontentId$global:huntingQueryCounter" -NotePropertyValue "$huntingQueryContentIdValue" $global:DependencyCriteria += [PSCustomObject]@{ kind = "HuntingQuery"; contentId = "[variables('huntingQueryObject$global:huntingQueryCounter')._huntingQuerycontentId$global:huntingQueryCounter]"; version = "[variables('huntingQueryObject$global:huntingQueryCounter').huntingQueryVersion$global:huntingQueryCounter]"; }; if ($global:huntingQueryCounter -eq 1) { if (!$(queryResourceExists) -and !$contentToImport.TemplateSpec) { $baseHuntingQueryResource = [PSCustomObject] @{ type = "Microsoft.OperationalInsights/workspaces"; apiVersion = $contentResourceDetails.huntingOperationalInsightsWorkspacesApiVersion; name = "[parameters('workspace')]"; location = "[parameters('workspace-location')]"; resources = @() } $global:baseMainTemplate.resources += $baseHuntingQueryResource } if (!$contentToImport.TemplateSpec -and $null -eq $global:baseMainTemplate.variables.'workspace-dependency') { #Add parser dependency variable once to ensure validation passes. $global: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" } } }) } $global: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 = $contentResourceDetails.savedSearchesApiVersion; #"2020-08-01"; name = $contentToImport.TemplateSpec ? "$($solutionName.Replace(' ', '_'))_Hunting_Query_$global:huntingQueryCounter" : "$solutionName Hunting Query $global:huntingQueryCounter"; location = "[parameters('workspace-location')]"; properties = [PSCustomObject] @{ eTag = "*"; displayName = $yaml.name; category = "Hunting Queries"; query = $yaml.query; version = $contentToImport.TemplateSpec ? 2 : 1; tags = @(); } } $huntingQueryDescription = "" if ($yaml.description) { #$huntingQueryDescription = $yaml.description.substring(1, $yaml.description.length - 3) $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) { if ($contentResourceDetails.apiVersion -eq '3.0.0') { $objHuntingQueryVariables | Add-Member -NotePropertyName "huntingQueryTemplateSpecName$global:huntingQueryCounter" -NotePropertyValue "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-hq-',uniquestring('$($huntingQueryContentIdValue)')))]" } else { $objHuntingQueryVariables | Add-Member -NotePropertyName "huntingQueryTemplateSpecName$global:huntingQueryCounter" -NotePropertyValue "[concat(parameters('workspace'),'-hq-',uniquestring('$($huntingQueryContentIdValue)'))]" } $global:baseMainTemplate.variables | Add-Member -NotePropertyName "huntingQueryObject$global:huntingQueryCounter" -NotePropertyValue $objHuntingQueryVariables if (!$global:baseMainTemplate.variables.workspaceResourceId -and $contentResourceDetails.contentSchemaVersion -ne '3.0.0') { $global:baseMainTemplate.variables | Add-Member -NotePropertyName "workspaceResourceId" -NotePropertyValue "[resourceId('microsoft.OperationalInsights/Workspaces', parameters('workspace'))]" } if ($contentResourceDetails.contentSchemaVersion -ne '3.0.0') { $baseHuntingQueryTemplateSpec = [PSCustomObject]@{ type = $contentResourceDetails.resourcetype; # "Microsoft.Resources/templateSpecs"; apiVersion = $contentResourceDetails.templateSpecsApiVersion; #"2022-02-01"; name = "[variables('huntingQueryObject$global:huntingQueryCounter').huntingQueryTemplateSpecName$global:huntingQueryCounter]"; location = "[parameters('workspace-location')]"; tags = [PSCustomObject]@{ "hidden-sentinelWorkspaceId" = "[variables('workspaceResourceId')]"; "hidden-sentinelContentType" = "HuntingQuery"; }; properties = [PSCustomObject]@{ description = "$($solutionName) Hunting Query $global:huntingQueryCounter with template"; displayName = "$($solutionName) Hunting Query template"; } } if($baseHuntingQueryTemplateSpec.properties.displayName.length -ge 64) { $baseHuntingQueryTemplateSpec.properties.displayName = "$($solutionName) HQ template"; } $global: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 = $contentResourceDetails.commonResourceMetadataApiVersion; #"2022-01-01-preview"; name = "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('HuntingQuery-', last(split(resourceId('Microsoft.OperationalInsights/savedSearches', variables('huntingQueryObject$global:huntingQueryCounter')._huntingQuerycontentId$global:huntingQueryCounter),'/'))))]"; properties = [PSCustomObject]@{ description = "$($solutionName) Hunting Query $global:huntingQueryCounter"; parentId = "[resourceId('Microsoft.OperationalInsights/savedSearches', variables('huntingQueryObject$global:huntingQueryCounter')._huntingQuerycontentId$global:huntingQueryCounter)]"; contentId = "[variables('huntingQueryObject$global:huntingQueryCounter')._huntingQuerycontentId$global:huntingQueryCounter]"; kind = "HuntingQuery"; version = "[variables('huntingQueryObject$global:huntingQueryCounter').huntingQueryVersion$global: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 = $contentResourceDetails.subtype; # "Microsoft.Resources/templateSpecs/versions"; apiVersion = $contentResourceDetails.templateSpecsVersionApiVersion; name = $contentResourceDetails.apiVersion -eq '3.0.0' ? "[variables('huntingQueryObject$global:huntingQueryCounter').huntingQueryTemplateSpecName$global:huntingQueryCounter]" : "[concat(variables('huntingQueryObject$global:huntingQueryCounter').huntingQueryTemplateSpecName$global:huntingQueryCounter,'/',variables('huntingQueryObject$global:huntingQueryCounter').huntingQueryVersion$global:huntingQueryCounter]"; location = "[parameters('workspace-location')]"; tags = [PSCustomObject]@{ "hidden-sentinelWorkspaceId" = "[variables('workspaceResourceId')]"; "hidden-sentinelContentType" = "HuntingQuery"; }; dependsOn = $contentResourceDetails.apiVersion -eq '3.0.0' ? @( "$($contentResourceDetails.dependsOn)") : @( "$($contentResourceDetails.dependsOn), variables('huntingQueryObject$global:huntingQueryCounter').huntingQueryTemplateSpecName$global: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('huntingQueryObject$global:huntingQueryCounter').huntingQueryVersion$global:huntingQueryCounter]"; parameters = [PSCustomObject]@{}; variables = [PSCustomObject]@{}; resources = @( # workbook $huntingQueryObj, # Metadata $huntingQueryMetadata ) }; } } if ($contentResourceDetails.apiVersion -eq '3.0.0') { $huntingQueryTemplateSpecContent = SetContentTemplateDefaultValuesToProperties -templateSpecResourceObj $huntingQueryTemplateSpecContent $huntingQueryTemplateSpecContent.properties.contentId = "[variables('huntingQueryObject$global:huntingQueryCounter')._huntingQuerycontentId$global:huntingQueryCounter]" $huntingQueryTemplateSpecContent.properties.contentKind = "HuntingQuery" $huntingQueryTemplateSpecContent.properties.displayName = $yaml.name $huntingQueryContentProductIdValue = "[concat(take(variables('_solutionId'),50),'-','hq','-', uniqueString(concat(variables('_solutionId'),'-','HuntingQuery','-',variables('huntingQueryObject$global:huntingQueryCounter')._huntingQuerycontentId$global:huntingQueryCounter,'-', '$($calculatedHuntingQueryVersion)')))]" $huntingQueryTemplateSpecContent.properties.contentProductId = "$huntingQueryContentProductIdValue" $huntingQueryTemplateSpecContent.properties.id = "$huntingQueryContentProductIdValue" $huntingQueryTemplateSpecContent.properties.version = "$calculatedHuntingQueryVersion" $huntingQueryTemplateSpecContent.PSObject.Properties.Remove('tags') } $global:baseMainTemplate.resources += $huntingQueryTemplateSpecContent } else{ if(!$contentToImport.TemplateSpec) { $dependsOn = @( "[variables('workspace-dependency')]" ); $huntingQueryObj | Add-Member -NotePropertyName "dependsOn" -NotePropertyValue $dependsOn } $queryLocation = $(getQueryResourceLocation) if ($null -eq $queryLocation) { Write-Host "For Hunting queries getQueryResourceLocation function returned null for $file which is not valid!" return; } else { $global:baseMainTemplate.resources[$(getQueryResourceLocation)].resources += $huntingQueryObj } } $dependencyDescription = "" if ($yaml.requiredDataConnectors) { $dependencyDescription = "This hunting query depends on $($yaml.requiredDataConnectors.connectorId) data connector ($($($yaml.requiredDataConnectors.dataTypes)) Parser or Table)" } $huntingQueryElement = [PSCustomObject]@{ name = "huntingquery$global:huntingQueryCounter"; type = "Microsoft.Common.Section"; label = $yaml.name; elements = @() } $huntingQueryElementDescription = [PSCustomObject]@{ name = "huntingquery$global:huntingQueryCounter-text"; type = "Microsoft.Common.TextBlock"; options = [PSCustomObject]@{ text = "$($huntingQueryDescription)$dependencyDescription"; } } if ($huntingQueryDescription -or $dependencyDescription) { $huntingQueryElement.elements += $huntingQueryElementDescription } $global:baseCreateUiDefinition.parameters.steps[$global:baseCreateUiDefinition.parameters.steps.Count - 1].elements += $huntingQueryElement # Update HuntingQuery Counter $global:huntingQueryCounter += 1 } function GenerateAlertRule($file, $contentResourceDetails) { # If yaml and not hunting query, process as Alert Rule Write-Host "Generating Alert Rule using $file" if ($global: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"; } } } ) } $global:baseCreateUiDefinition.parameters.steps += $baseAnalyticRuleStep } $yamlPropertiesToCopyFrom = "name", "severity", "triggerThreshold", "query" $yamlPropertiesToCopyTo = "displayName", "severity", "triggerThreshold", "query" $alertRuleParameterName = "analytic$global: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." } $calculatedAnalyticRuleVersion = ($null -ne $yaml.version) ? "$($yaml.version)" : "1.0.0" $objAnalyticRulesVariables = [pscustomobject]@{} $objAnalyticRulesVariables | Add-Member -NotePropertyName "analyticRuleVersion$global:analyticRuleCounter" -NotePropertyValue "$calculatedAnalyticRuleVersion" $objAnalyticRulesVariables | Add-Member -NotePropertyName "_analyticRulecontentId$global:analyticRuleCounter" -NotePropertyValue "$($yaml.id)" $global:DependencyCriteria += [PSCustomObject]@{ kind = "AnalyticsRule"; contentId = "[variables('analyticRuleObject$global:analyticRuleCounter')._analyticRulecontentId$global:analyticRuleCounter]"; #post bug bash ,remove this below comments! version = "[variables('analyticRuleObject$global:analyticRuleCounter').analyticRuleVersion$global: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 @(); } 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 } $subtechniques = @() $techniques = @() if ($yaml.relevantTechniques -and ($yaml.relevantTechniques.Count -gt 0) ) { if ($yaml.relevantTechniques -match ' ') { $yaml.relevantTechniques = $yaml.relevantTechniques -replace ' ', '' } foreach ($item in [array]($yaml.relevantTechniques)) { if ($item.contains('.')) { if (!$subtechniques.Contains($item)) { $subtechniques += $item } } if (!$techniques.Contains($item)) { $techniques += ($item -split "\.")[0] } } } if ($subtechniques.Count -gt 0) { Write-Host "Has Sub-Techniques for Analytic rule file name $fileName" $alertRule | Add-Member -NotePropertyName subTechniques -NotePropertyValue ([array]($subtechniques)) } if ($techniques.Count -gt 0) { Write-Host "Has Techniques for Analytic rule file name $fileName" $alertRule | Add-Member -NotePropertyName techniques -NotePropertyValue ([array]($techniques)) } # 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); } elseif($yamlField -eq "sentinelEntitiesMappings" -and $yaml.$yamlField.length -lt 2) { $alertRule.sentinelEntitiesMappings = @($alertRule.sentinelEntitiesMappings); } } } # 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$global:analyticRuleCounter-id'))]"; apiVersion = $contentResourceDetails.alertRuleApiVersion; #"2022-04-01-preview"; kind = "$($yaml.kind)"; location = "[parameters('workspace-location')]"; properties = $alertRule; } if($contentToImport.TemplateSpec) { $objAnalyticRulesVariables | Add-Member -NotePropertyName "analyticRuleId$global:analyticRuleCounter" -NotePropertyValue "[resourceId('Microsoft.SecurityInsights/AlertRuleTemplates', '$($yaml.id)')]" if ($contentResourceDetails.apiVersion -eq '3.0.0') { $objAnalyticRulesVariables | Add-Member -NotePropertyName "analyticRuleTemplateSpecName$global:analyticRuleCounter" -NotePropertyValue "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-ar-',uniquestring('$($yaml.id)')))]" } else { $objAnalyticRulesVariables | Add-Member -NotePropertyName "analyticRuleTemplateSpecName$global:analyticRuleCounter" -NotePropertyValue "[concat(parameters('workspace'),'-ar-',uniquestring('$($yaml.id)'))]" } if (!$global:baseMainTemplate.variables.workspaceResourceId -and $contentResourceDetails.contentSchemaVersion -ne '3.0.0') { $global:baseMainTemplate.variables | Add-Member -NotePropertyName "workspaceResourceId" -NotePropertyValue "[resourceId('microsoft.OperationalInsights/Workspaces', parameters('workspace'))]" } if ($contentResourceDetails.contentSchemaVersion -ne '3.0.0') { $baseAnalyticRuleTemplateSpec = [PSCustomObject]@{ type = $contentResourceDetails.resourcetype; # "Microsoft.Resources/templateSpecs"; apiVersion = $contentResourceDetails.templateSpecsApiVersion; #"2022-02-01"; name = "[variables('analyticRuleObject$global:analyticRuleCounter').analyticRuleTemplateSpecName$global:analyticRuleCounter]"; location = "[parameters('workspace-location')]"; tags = [PSCustomObject]@{ "hidden-sentinelWorkspaceId" = "[variables('workspaceResourceId')]"; "hidden-sentinelContentType" = "AnalyticsRule"; }; properties = [PSCustomObject]@{ description = "$($solutionName) Analytics Rule $global:analyticRuleCounter with template"; displayName = "$($solutionName) Analytics Rule template"; } } if ($baseAnalyticRuleTemplateSpec.properties.displayName.length -ge 64) { $baseAnalyticRuleTemplateSpec.properties.displayName = "$($solutionName) AR template"; } $global:baseMainTemplate.resources += $baseAnalyticRuleTemplateSpec } $newAnalyticRule.name = "[variables('analyticRuleObject$global:analyticRuleCounter')._analyticRulecontentId$global:analyticRuleCounter]" $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 = $contentResourceDetails.commonResourceMetadataApiVersion; #"2022-01-01-preview"; name = "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('AnalyticsRule-', last(split(variables('analyticRuleObject$global:analyticRuleCounter').analyticRuleId$global:analyticRuleCounter,'/'))))]"; properties = [PSCustomObject]@{ description = "$($solutionName) Analytics Rule $global:analyticRuleCounter"; parentId = "[variables('analyticRuleObject$global:analyticRuleCounter').analyticRuleId$global:analyticRuleCounter]"; contentId = "[variables('analyticRuleObject$global:analyticRuleCounter')._analyticRulecontentId$global:analyticRuleCounter]"; kind = "AnalyticsRule"; # Need to remove the below assigned property for the yaml version after bug bash version = "[variables('analyticRuleObject$global:analyticRuleCounter').analyticRuleVersion$global: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 = $contentResourceDetails.subtype; # "Microsoft.Resources/templateSpecs/versions"; apiVersion = $contentResourceDetails.templateSpecsVersionApiVersion; name = $contentResourceDetails.apiVersion -eq '3.0.0' ? "[variables('analyticRuleObject$global:analyticRuleCounter').analyticRuleTemplateSpecName$global:analyticRuleCounter]" : "[concat(variables('analyticRuleObject$global:analyticRuleCounter').analyticRuleTemplateSpecName$global:analyticRuleCounter,'/',variables('analyticRuleObject$global:analyticRuleCounter').analyticRuleVersion$global:analyticRuleCounter)]"; location = "[parameters('workspace-location')]"; tags = [PSCustomObject]@{ "hidden-sentinelWorkspaceId" = "[variables('workspaceResourceId')]"; "hidden-sentinelContentType" = "AnalyticsRule"; }; dependsOn = $contentResourceDetails.apiVersion -eq '3.0.0' ? @( "$($contentResourceDetails.dependsOn)") : @( "$($contentResourceDetails.dependsOn), variables('analyticRuleObject$global:analyticRuleCounter').analyticRuleTemplateSpecName$global: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('analyticRuleObject$global:analyticRuleCounter').analyticRuleVersion$global:analyticRuleCounter]"; parameters = [PSCustomObject]@{}; variables = [PSCustomObject]@{}; resources = @( # Analytics Rule $newAnalyticRule, # Metadata $analyticRuleMetadata ) }; } } if ($contentResourceDetails.apiVersion -eq '3.0.0') { $analyticRuleTemplateSpecContent = SetContentTemplateDefaultValuesToProperties -templateSpecResourceObj $analyticRuleTemplateSpecContent $analyticRuleTemplateSpecContent.properties.contentId = "[variables('analyticRuleObject$global:analyticRuleCounter')._analyticRulecontentId$global:analyticRuleCounter]" $analyticRuleTemplateSpecContent.properties.contentKind = "AnalyticsRule" $analyticRuleTemplateSpecContent.properties.displayName = $yaml.name $objAnalyticRulesVariables | Add-Member -NotePropertyName "_analyticRulecontentProductId$global:analyticRuleCounter" -NotePropertyValue "[concat(take(variables('_solutionId'),50),'-','ar','-', uniqueString(concat(variables('_solutionId'),'-','AnalyticsRule','-','$($yaml.id)','-', '$calculatedAnalyticRuleVersion')))]" $analyticRuleTemplateSpecContent.properties.contentProductId = "[variables('analyticRuleObject$global:analyticRuleCounter')._analyticRulecontentProductId$global:analyticRuleCounter]" $analyticRuleTemplateSpecContent.properties.id = "[variables('analyticRuleObject$global:analyticRuleCounter')._analyticRulecontentProductId$global:analyticRuleCounter]" $analyticRuleTemplateSpecContent.properties.version = "[variables('analyticRuleObject$global:analyticRuleCounter').analyticRuleVersion$global:analyticRuleCounter]" $analyticRuleTemplateSpecContent.PSObject.Properties.Remove('tags') } $global:baseMainTemplate.resources += $analyticRuleTemplateSpecContent } else { # Add Resource and Parameters to Template $global:baseMainTemplate.resources += $newAnalyticRule } $global:baseMainTemplate.variables | Add-Member -NotePropertyName "analyticRuleObject$global:analyticRuleCounter" -NotePropertyValue $objAnalyticRulesVariables if(!$contentToImport.TemplateSpec) { $global:baseMainTemplate.parameters | Add-Member -MemberType NoteProperty -Name $alertRuleParameterName -Value $alertRuleParameter } $alertRuleUIParameter = [PSCustomObject] @{ name = "analytic$global:analyticRuleCounter"; type = "Microsoft.Common.Section"; label = $alertRule.displayName; elements = @( [PSCustomObject] @{ name = "analytic$global:analyticRuleCounter-text"; type = "Microsoft.Common.TextBlock"; options = @{ text = $alertRule.description; } } ) } $global:baseCreateUiDefinition.parameters.steps[$global:baseCreateUiDefinition.parameters.steps.Count - 1].elements += $alertRuleUIParameter # Update Counter $global:analyticRuleCounter += 1 } function GenerateWatchList($json, $isPipelineRun, $watchListFileName) { $watchlistData = $json.resources[0] $watchlistName = $watchlistData.properties.displayName; if ($contentToImport.Metadata -or $isPipelineRun) { $global:baseMainTemplate.variables | Add-Member -NotePropertyName $watchlistName -NotePropertyValue $watchlistName $global:baseMainTemplate.variables | Add-Member -NotePropertyName "_$watchlistName" -NotePropertyValue "[variables('$watchlistName')]" } $global:DependencyCriteria += [PSCustomObject]@{ kind = "Watchlist"; contentId = "[variables('_$watchlistName')]"; version = $contentToImport.Version; }; #Handle CreateUiDefinition Base Step if ($global: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"; } } } ) } $global:baseCreateUiDefinition.parameters.steps += $baseWatchlistStep } #Handle CreateUiDefinition Step Sub-Element $watchlistDescriptionText = $(if ($contentToImport.WatchlistDescription -and $contentToImport.WatchlistDescription -is [System.Array]) { $contentToImport.WatchlistDescription[$global:watchlistCounter - 1] } elseif ($contentToImport.WatchlistDescription -and $contentToImport.WatchlistDescription -is [System.String]) { $contentToImport.WatchlistDescription } else { $watchlistData.properties.description }) $currentStepNum = $global:baseCreateUiDefinition.parameters.steps.Count - 1 $watchlistStepElement = [PSCustomObject]@{ name = "watchlist$global:watchlistCounter"; type = "Microsoft.Common.Section"; label = $watchlistData.properties.displayName; elements = @( [PSCustomObject]@{ name = "watchlist$global:watchlistCounter-text"; type = "Microsoft.Common.TextBlock"; options = [PSCustomObject]@{ text = $watchlistDescriptionText } } ) } $global:baseCreateUiDefinition.parameters.steps[$currentStepNum].elements += $watchlistStepElement # Add Watchlist ID to MainTemplate parameters $watchlistIdParameterName = "watchlist$global:watchlistCounter-id" $watchlistIdParameter = [PSCustomObject] @{ type = "string"; defaultValue = "$watchListFileName"; minLength = 1; metadata = [PSCustomObject] @{ description = "Unique id for the watchlist" }; } $global:baseMainTemplate.parameters | Add-Member -MemberType NoteProperty -Name $watchlistIdParameterName -Value $watchlistIdParameter # Replace watchlist resource id $watchlistData.name = "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',parameters('watchlist$global:watchlistCounter-id'))]" # Handle MainTemplate Resource $global:baseMainTemplate.resources += $watchlistData #Assume 1 watchlist per template # Update Watchlist Counter $global:watchlistCounter += 1 } function GenerateSavedSearches($json, $contentResourceDetails) { $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$global:savedSearchCounter-id" $savedSearchIdParameter = [PSCustomObject] @{ type = "string"; defaultValue = "[newGuid()]"; minLength = 1; metadata = [PSCustomObject] @{ description = "Unique id for the watchlist" }; } $global:baseMainTemplate.parameters | Add-Member -MemberType NoteProperty -Name $savedSearchIdParameterName -Value $savedSearchIdParameter $savedSearchResource = [PSCustomObject]@{ type = "Microsoft.OperationalInsights/workspaces/savedSearches"; apiVersion = $contentResourceDetails.savedSearchesApiVersion; #"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; }; } $global:baseMainTemplate.resources += $savedSearchResource $global:savedSearchCounter++ } } elseif ($isStandardTemplate) { $global:baseMainTemplate.resources += $searchData } } function GenerateParsersList($file, $contentToImport, $contentResourceDetails) { # Assume file is Parser due to parsers having inconsistent types. (.txt, .kql, or none) Write-Host "Generating Data Parser using $file" generateParserContent $file $contentToImport $contentResourceDetails } function RunArmTtkOnPackage { param( [Parameter(Mandatory=$True)] [string]$solutionName, [Parameter(Mandatory=$True)] [System.Boolean]$isPipelineRun ) if ($isPipelineRun -eq $false) { #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'" } } # this function is to check if maintemplate and createUi files in package folder is valid or not function CheckJsonIsValid($solutionFolderBasePath) { $packageFolderPath = "$solutionFolderBasePath\Package\*.json" if ((Test-Path -Path "$packageFolderPath")) { [array]$packageFolderFiles = Get-ChildItem "$packageFolderPath" -Recurse| select -expand fullname Write-Host "************Validating if Package Json files are valid or not***************" foreach ($filePath in $packageFolderFiles) { $isValid = Get-Content "$filePath" -Raw | Test-Json -ErrorAction SilentlyContinue if ($isValid) { Write-Host "File $filePath is a valid Json file!" } else { Write-Host "File $filePath is Not a valid Json file!" } } } } function GeneratePackage($solutionName, $contentToImport, $calculatedBuildPipelinePackageVersion = '') { if ($contentToImport.Description) { $global:baseCreateUiDefinition.parameters.config.basics.description = $global:baseCreateUiDefinition.parameters.config.basics.description -replace "{{SolutionDescription}}", $contentToImport.Description $global:baseCreateUiDefinition.parameters.config.basics.description = $global:baseCreateUiDefinition.parameters.config.basics.description -replace "{{SolutionName}}", [URI]::EscapeDataString($solutionName) } else { $global:baseCreateUiDefinition.parameters.config.basics.description = $global:baseCreateUiDefinition.parameters.config.basics.description -replace "{{SolutionDescription}}", "" } # Update the descriptionHtml field and icon field only for the content templates # and for resource type /contenttemplates if ($contentResourceDetails.apiVersion -eq '3.0.0') { $updatecontentpackage = $baseMainTemplate.resources | Where-Object {$_.type -eq "Microsoft.OperationalInsights/workspaces/providers/contentPackages"} if ($updatecontentpackage -is [System.Object[]]) { foreach($contentPackageItem in $updatecontentpackage) { $descriptionHtml = $global:baseCreateUiDefinition.parameters.config.basics.description -replace "{{Logo}}\n\n", "" $md = $descriptionHtml | ConvertFrom-Markdown $contentPackageItem.properties.descriptionHtml = $md.Html $contentPackageItem.properties.icon = $contentToImport.Logo } } else { $descriptionHtml = $global:baseCreateUiDefinition.parameters.config.basics.description -replace "{{Logo}}\n\n", "" $md = $descriptionHtml | ConvertFrom-Markdown $updatecontentpackage.properties.descriptionHtml = $md.Html $updatecontentpackage.properties.icon = $contentToImport.Logo } } # Update Logo in CreateUiDefinition Description if ($contentToImport.Logo) { $global:baseCreateUiDefinition.parameters.config.basics.description = $global:baseCreateUiDefinition.parameters.config.basics.description -replace "{{Logo}}", $contentToImport.Logo } else { $global:baseCreateUiDefinition.parameters.config.basics.description = $global: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 # create testParameters json file which will be used for test case automation $testParametersOutputPath = "$solutionFolder/testParameters.json" $baseMainTemplate.parameters | ConvertTo-Json -Depth $jsonConversionDepth | Out-File $testParametersOutputPath -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" $global:baseCreateUiDefinition.parameters.steps = $global:baseCreateUiDefinition.parameters.steps | Sort-Object { $createUiDefinitionOrder.IndexOf($_.name) } # Ensure single-step UI Definitions have proper type for steps if ($null -ne $global:baseCreateUiDefinition.parameters.steps -and $($global:baseCreateUiDefinition.parameters.steps).GetType() -ne [System.Object[]]) { $global:baseCreateUiDefinition.parameters.steps = @($global:baseCreateUiDefinition.parameters.steps) } elseif ($null -eq $global:baseCreateUiDefinition.parameters.steps) { # when there is no content then create ui fails as step is null $global:baseCreateUiDefinition.parameters.steps = @(@{}) # [{}] } $global:baseCreateUiDefinition | ConvertTo-Json -Depth $jsonConversionDepth | Out-File $createUiDefinitionOutputPath -Encoding utf8 } catch { Write-Host "Failed to write output file $createUiDefinitionOutputPath" -ForegroundColor Red break; } if ($calculatedBuildPipelinePackageVersion -eq '') { $zipPackageName = "$(if($contentToImport.Version){$contentToImport.Version}else{"newSolutionPackage"}).zip" } else { $zipPackageName = "$calculatedBuildPipelinePackageVersion" + ".zip" } $compress = @{ Path = "$solutionFolder/createUiDefinition.json", "$solutionFolder/mainTemplate.json" DestinationPath = "$solutionFolder/$zipPackageName" } Compress-Archive @compress -Force } function global:GetContentTemplateDefaultValues() { return @{ 'packageKind' = "Solution" 'packageVersion' = "[variables('_solutionVersion')]" 'packageName' = "[variables('_solutionName')]" 'packageId' = "[variables('_solutionId')]" 'contentSchemaVersion' = '3.0.0' } } function global:SetContentTemplateDefaultValuesToProperties($templateSpecResourceObj) { $contentTemplateDefaultValues = GetContentTemplateDefaultValues $templateSpecResourceObj.properties | Add-Member -MemberType NoteProperty -Name "packageKind" -Value $contentTemplateDefaultValues.packageKind $templateSpecResourceObj.properties | Add-Member -MemberType NoteProperty -Name "packageVersion" -Value $contentTemplateDefaultValues.packageVersion $templateSpecResourceObj.properties | Add-Member -MemberType NoteProperty -Name "packageName" -Value $contentTemplateDefaultValues.packageName $templateSpecResourceObj.properties | Add-Member -MemberType NoteProperty -Name "packageId" -Value $contentTemplateDefaultValues.packageId $templateSpecResourceObj.properties | Add-Member -MemberType NoteProperty -Name "contentSchemaVersion" -Value $contentTemplateDefaultValues.contentSchemaVersion $templateSpecResourceObj.properties | Add-Member -MemberType NoteProperty -Name "contentId" -Value "" $templateSpecResourceObj.properties | Add-Member -MemberType NoteProperty -Name "contentKind" -Value "" $templateSpecResourceObj.properties | Add-Member -MemberType NoteProperty -Name "displayName" -Value "" $templateSpecResourceObj.properties | Add-Member -MemberType NoteProperty -Name "contentProductId" -Value "" $templateSpecResourceObj.properties | Add-Member -MemberType NoteProperty -Name "id" -Value "" $templateSpecResourceObj.properties | Add-Member -MemberType NoteProperty -Name "version" -Value "" return $templateSpecResourceObj } # =================START: RESOURCE CONTENT BY VERSION===================== function global:returnContentResources ($item) { try { $dict = $null; $version = constructVersionNumber($item) if($version.Major -eq 3) { $dict = @{ #'resourcetype' = "Microsoft.OperationInsights/workspaces/providers/contentPackages" 'subtype' = "Microsoft.OperationalInsights/workspaces/providers/contentTemplates" 'dependsOn' = "[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]" 'metadata' = "Microsoft.OperationalInsights/workspaces/providers/contentPackages" 'contentSchemaVersion' = '3.0.0' 'apiVersion' = '3.0.0' 'metadataApiVersion' = '2023-04-01-preview' 'templateSpecsVersionApiVersion' = '2023-04-01-preview' 'resources' = @("Microsoft.OperationalInsights/workspaces/providers/dataConnectors", "Microsoft.OperationalInsights/workspaces/providers/metadata", "Microsoft.OperationalInsights/workspaces/savedSearches", "Microsoft.OperationalInsights/workspaces/providers/Watchlists", "Microsoft.OperationalInsights/workspaces/providers/contentTemplates", "Microsoft.OperationalInsights/workspaces/providers/contentPackages", "Microsoft.OperationalInsights/workspaces/providers/dataConnectorDefinitions") } } elseif ($version.Major -eq 2) { $dict = @{ 'resourcetype' = "Microsoft.Resources/templateSpecs" 'subtype' = "Microsoft.Resources/templateSpecs/versions" 'dependsOn' = "[resourceId('Microsoft.Resources/templateSpecs'" 'metadata' = "Microsoft.OperationalInsights/workspaces/providers/metadata" 'contentSchemaVersion' = '2.0.0' 'apiVersion' = '2.0.0' 'metadataApiVersion' = '2022-01-01-preview' 'templateSpecsVersionApiVersion' = '2022-02-01' 'resources' = @("Microsoft.OperationalInsights/workspaces/providers/dataConnectors", "Microsoft.OperationalInsights/workspaces/providers/metadata", "Microsoft.OperationalInsights/workspaces/savedSearches", "Microsoft.OperationalInsights/workspaces/providers/Watchlists", "Microsoft.Resources/templateSpecs", "Microsoft.Resources/templateSpecs/versions") } } else { Write-Host "Returning Empty dictionary object because the version information is given as null or empty" -ForegroundColor Red return $dict } if ($null -ne $dict) { # ADD COMMON API VERSION $dict.Add('templateSpecsApiVersion', '2022-02-01') $dict.Add('dataConnectorsApiVersion', '2021-03-01-preview') $dict.Add('huntingOperationalInsightsWorkspacesApiVersion', '2021-06-01') $dict.Add('parserOperationalInsightsWorkspacesApiVersion', '2020-08-01') $dict.Add('savedSearchesApiVersion', '2022-10-01') $dict.Add('alertRuleApiVersion', '2023-02-01-preview') $dict.Add('commonResourceMetadataApiVersion', '2022-01-01-preview') $dict.Add('insightsWorkbookApiVersion', '2021-08-01') } return $dict } catch { Write-Host "Expected Version information is coming as null. Please correct the version number accordingly: $($_.Exception.Message)" -ForegroundColor Red break; } } function global:constructVersionNumber($version) { $major,$minor,$build,$revision = $version.split(".") $version = [version]::new($major,$minor,$build) return $version } # =================END: RESOURCE CONTENT BY VERSION===================== function ConvertHashTo-StringData { [CmdletBinding()] param( [Parameter(Mandatory, Position = 0, ValueFromPipeline)] [HashTable[]]$HashTable ) $Parameters = "" foreach ($item in $HashTable) { if($Parameters -ne "") { $Parameters = $Parameters + "," } if($item.Type.StartsWith('table:')) { $Parameters = $Parameters + "{0}:{1}" -f $item.Name, $item.Type.split(":")[-1] } else { if($null -ne $item.Default) { $Default = $item.Default if($item.Type.ToLower() -eq 'string') { $Default = "'$default'" } $Parameters = $Parameters + "{0}:{1}={2}" -f $item.Name, $item.Type,$Default } else { $Parameters = $Parameters + "{0}:{1}" -f $item.Name, $item.Type } } } return $Parameters } function global:GenerateIdHash($packageId, $contentKind, $contentId, $contentVersion) { if ([string]::IsNullOrWhiteSpace($packageId) -or [string]::IsNullOrEmpty($contentId)) { return [string]::Empty } $sb = [System.Text.StringBuilder]::new() [int]$limit = 50 $sb.Append($packageId.Length -gt $limit ? $packageId.Substring(0, $limit) : $packageId).Append('-') if ($ContentKindDict.ContainsKey($contentKind)) { $sb.Append($ContentKindDict[$contentKind]) } else { $sb.Append($contentKind.ToString().substring(0,2).tolower()) } $sb.Append('-') $str = "$packageId-$($contentKind.ToString())-$contentId"; if ([string]::IsNullOrEmpty($contentVersion)) { $str += "-$contentVersion"; } $val = HashLine -value $str $sb.Append($val) return $sb.ToString() } function HashLine($value) { $val = [string](Base32Encode -charValue "$(ComputeSHA($value))") return $val.ToString() } function ComputeSHA { Param ( [Parameter(Mandatory=$true)] [string] $ClearString ) $hasher = [System.Security.Cryptography.HashAlgorithm]::Create('sha256') $hash = $hasher.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($ClearString)) $hashString = [System.BitConverter]::ToInt32($hash) return $hashString } function Base32Encode([uint32]$charValue) { $chars = "abcdefghijklmnopqrstuvwxyz234567" $Charset = ($chars).ToCharArray() $sb = [System.Text.StringBuilder]::new() for($index = 0; $index -lt 13; $index++) { $sb.Append($Charset[[int]($charValue -shr 59)]) $charValue -shl 5 } return $sb.ToString() } function addTemplateSpecParserResource($content,$yaml,$isyaml, $contentResourceDetails) { # Add workspace resource ID if not available if (!$global:baseMainTemplate.variables.workspaceResourceId -and $contentResourceDetails.contentSchemaVersion -ne '3.0.0') { $global:baseMainTemplate.variables | Add-Member -NotePropertyName "workspaceResourceId" -NotePropertyValue "[resourceId('microsoft.OperationalInsights/Workspaces', parameters('workspace'))]" } if ($contentResourceDetails.contentSchemaVersion -ne '3.0.0') { # Add base templateSpec $baseParserTemplateSpec = [PSCustomObject]@{ type = "Microsoft.Resources/templateSpecs"; apiVersion = $contentResourceDetails.templateSpecsApiVersion; #"2022-02-01"; name = "[variables('parserObject$global:parserCounter').parserTemplateSpecName$global: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"; } } $global:baseMainTemplate.resources += $baseParserTemplateSpec } # Parser Content $parserContent = [PSCustomObject]@{ name = "[variables('parserObject$global:parserCounter')._parserName$global:parserCounter]"; apiVersion = $contentResourceDetails.savedSearchesApiVersion; #"2020-08-01"; type = "Microsoft.OperationalInsights/workspaces/savedSearches"; location = "[parameters('workspace-location')]"; properties = [PSCustomObject]@{ eTag = "*" displayName = "$($displayDetails.displayName)" category = $isyaml ? "$($yaml.Category)" : "Samples" functionAlias = "$($displayDetails.functionAlias)" query = $isyaml ? "$($yaml.FunctionQuery)" : "$content" functionParameters = $isyaml -and $null -ne $yaml.FunctionParams ? "$(ConvertHashTo-StringData $yaml.FunctionParams)" : "" version = $isyaml ? 2 : 1 tags = @([PSCustomObject]@{ "name" = "description" "value" = $isyaml ? "$($yaml.Description)" : "$($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 = $contentResourceDetails.commonResourceMetadataApiVersion; #"2022-01-01-preview"; name = "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('Parser-', last(split(variables('parserObject$global:parserCounter')._parserId$global:parserCounter,'/'))))]"; dependsOn = @( "[variables('parserObject$global:parserCounter')._parserId$global:parserCounter]" ); properties = [PSCustomObject]@{ parentId = "[resourceId('Microsoft.OperationalInsights/workspaces/savedSearches', parameters('workspace'), '$($displayDetails.name)')]" contentId = "[variables('parserObject$global:parserCounter').parserContentId$global:parserCounter]"; kind = "Parser"; version = "[variables('parserObject$global:parserCounter').parserVersion$global: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 = $contentResourceDetails.subtype; #"Microsoft.Resources/templateSpecs/versions"; apiVersion = $contentResourceDetails.templateSpecsVersionApiVersion; name = $contentResourceDetails.apiVersion -eq '3.0.0' ? "[variables('parserObject$global:parserCounter').parserTemplateSpecName$global:parserCounter]" : "[concat(variables('parserObject$global:parserCounter').parserTemplateSpecName$global:parserCounter,'/',variables('parserObject$global:parserCounter').parserVersion$global:parserCounter)]"; location = "[parameters('workspace-location')]"; tags = [PSCustomObject]@{ "hidden-sentinelWorkspaceId" = "[variables('workspaceResourceId')]"; "hidden-sentinelContentType" = "Parser"; }; dependsOn = $contentResourceDetails.apiVersion -eq '3.0.0' ? @( "$($contentResourceDetails.dependsOn)") : @( "[resourceId('Microsoft.Resources/templateSpecs', variables('parserObject$global:parserCounter').parserTemplateSpecName$global: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('parserObject$global:parserCounter').parserVersion$global:parserCounter]"; parameters = [PSCustomObject]@{}; variables = [PSCustomObject]@{}; resources = @( # Parser $parserContent, # Metadata $parserMetadata ) }; } } if ($contentResourceDetails.apiVersion -eq '3.0.0') { $parserTemplateSpecContent = SetContentTemplateDefaultValuesToProperties -templateSpecResourceObj $parserTemplateSpecContent $parserTemplateSpecContent.properties.contentId = "[variables('parserObject$global:parserCounter').parserContentId$global:parserCounter]" $parserTemplateSpecContent.properties.contentKind = "Parser" $parserTemplateSpecContent.properties.displayName = "$($displayDetails.displayName)" #$global:baseMainTemplate.variables | Add-Member -NotePropertyName "_parsercontentProductId$global:parserCounter" -NotePropertyValue "[concat(take(variables('_solutionId'),50),'-','$($ContentKindDict.ContainsKey("Parser") ? $ContentKindDict["Parser"] : '')','-', uniqueString(concat(variables('_solutionId'),'-','Parser','-',variables('parserObject$global:parserCounter').parserContentId$global:parserCounter,'-', '$($global:parserVersion)')))]" $parserContentProductIdValue = "[concat(take(variables('_solutionId'),50),'-','$($ContentKindDict.ContainsKey("Parser") ? $ContentKindDict["Parser"] : '')','-', uniqueString(concat(variables('_solutionId'),'-','Parser','-',variables('parserObject$global:parserCounter').parserContentId$global:parserCounter,'-', '$($global:parserVersion)')))]" $parserTemplateSpecContent.properties.contentProductId = "$parserContentProductIdValue" $parserTemplateSpecContent.properties.id = "$parserContentProductIdValue" $parserTemplateSpecContent.properties.version = "[variables('parserObject$global:parserCounter').parserVersion$global:parserCounter]" $parserTemplateSpecContent.PSObject.Properties.Remove('tags') } $global:baseMainTemplate.resources += $parserTemplateSpecContent $parserObj = [PSCustomObject] @{ type = "Microsoft.OperationalInsights/workspaces/savedSearches"; apiVersion = $contentResourceDetails.savedSearchesApiVersion; #"2020-08-01"; name = "[variables('parserObject$global:parserCounter')._parserName$global:parserCounter]"; location = "[parameters('workspace-location')]"; properties = [PSCustomObject] @{ eTag = "*" displayName = "$($displayDetails.displayName)" category = $isyaml ? "$($yaml.Category)" :"Samples" functionAlias = "$($displayDetails.functionAlias)" query = $isyaml ? "$($yaml.FunctionQuery)" : "$content" functionParameters = $isyaml -and $null -ne $yaml.FunctionParams ? "$(ConvertHashTo-StringData $yaml.FunctionParams)" : "" version = $isyaml ? 2 : 1 tags = @([PSCustomObject]@{ "name" = "description" "value" = $isyaml ? "$($yaml.Description)" : "$($displayDetails.displayName)" }; ) } } $global:baseMainTemplate.resources += $parserObj $parserMetadata = [PSCustomObject]@{ type = "Microsoft.OperationalInsights/workspaces/providers/metadata"; apiVersion = $contentResourceDetails.commonResourceMetadataApiVersion #"2022-01-01-preview"; location = "[parameters('workspace-location')]"; name = "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat('Parser-', last(split(variables('parserObject$global:parserCounter')._parserId$global:parserCounter,'/'))))]"; dependsOn = @( "[variables('parserObject$global:parserCounter')._parserId$global:parserCounter]" ); properties = [PSCustomObject]@{ parentId = "[resourceId('Microsoft.OperationalInsights/workspaces/savedSearches', parameters('workspace'), '$($displayDetails.name)')]" contentId = "[variables('parserObject$global:parserCounter').parserContentId$global:parserCounter]"; kind = "Parser"; version = "[variables('parserObject$global:parserCounter').parserVersion$global:parserCounter]"; source = [PSCustomObject]@{ kind = "Solution"; name = $contentToImport.Name; sourceId = "[variables('_solutionId')]" }; author = $authorDetails; support = $baseMetadata.support } } $global:baseMainTemplate.resources += $parserMetadata } function generateParserContent($file, $contentToImport, $contentResourceDetails) { $objParserVariables = [pscustomobject]@{} $solutionName = $contentToImport.name if ($global:parserCounter -eq 1 -and $null -eq $global:baseMainTemplate.variables.'workspace-dependency' -and !$contentToImport.TemplateSpec) { # Add parser dependency variable once to ensure validation passes. $global:baseMainTemplate.variables | Add-Member -MemberType NoteProperty -Name "workspace-dependency" -Value "[concat('Microsoft.OperationalInsights/workspaces/', parameters('workspace'))]" } $fileName = Split-Path $file -leafbase; function getFileNameFromPath ([string]$inputFilePath) { # Split out path $indexOfSlashInFile = $inputFilePath.IndexOf("/") if ($indexOfSlashInFile -gt 0) { $output = $inputFilePath.Split("/") Write-Host "input file path is $inputFilePath output $output" $output = $output[$output.Length - 1] Write-Host "updated output $output" # Split out file type $output = $output.Split(".")[0] return $output } else { # GET NAME OF THE FILE WITHOUT ANY EXTENSION $output = $inputFilePath.Split(".")[0] return $output } } $content = '' $yaml = $null $isyaml = $false $rawData = $rawData.Split("`n") foreach ($line in $rawData) { # Remove comment lines before condensing query if (!$line.StartsWith("//")) { $content = $content + "`n" + $line } } if($file -match "(\.yaml)$") { try { $yaml = ConvertFrom-YAML $content $isyaml = $true } catch { Write-Host "Failed to deserialize while converting the Saved Search Function from Yaml $file" -ForegroundColor Red break; } } $displayDetails = getParserDetails $global:solutionId $yaml $isyaml $objParserVariables | Add-Member -NotePropertyName "_parserName$global:parserCounter" -NotePropertyValue "[concat(parameters('workspace'),'/','$($displayDetails.name)')]" $objParserVariables | Add-Member -NotePropertyName "_parserId$global:parserCounter" -NotePropertyValue "[resourceId('Microsoft.OperationalInsights/workspaces/savedSearches', parameters('workspace'), '$($displayDetails.name)')]" $functionAlias = ($null -ne $yaml -and $yaml.Count -gt 0) ? $yaml.FunctionName : "$($displayDetails.functionAlias)" $parserContentIdValue = "$($functionAlias)-Parser" if ($contentResourceDetails.apiVersion -eq '3.0.0') { $objParserVariables | Add-Member -NotePropertyName "parserTemplateSpecName$global:parserCounter" -NotePropertyValue "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/',concat(parameters('workspace'),'-pr-',uniquestring('$($parserContentIdValue)')))]" } else { $objParserVariables | Add-Member -NotePropertyName "parserTemplateSpecName$global:parserCounter" -NotePropertyValue "[concat(parameters('workspace'),'-pr-',uniquestring('$($parserContentIdValue)'))]" } # Use File Name as Parser Name $global:parserVersion = ($null -ne $yaml -and $yaml.Count -gt 0) ? ($null -eq $yaml.Function.Version ? "1.0.0" : $yaml.Function.Version) : "1.0.0" # create object with multiple values in it for a single parser resource $objParserVariables | Add-Member -NotePropertyName "parserVersion$global:parserCounter" -NotePropertyValue $global:parserVersion $objParserVariables | Add-Member -NotePropertyName "parserContentId$global:parserCounter" -NotePropertyValue "$parserContentIdValue" $global:baseMainTemplate.variables | Add-Member -NotePropertyName "parserObject$global:parserCounter" -NotePropertyValue $objParserVariables $global:DependencyCriteria += [PSCustomObject]@{ kind = "Parser"; contentId = "[variables('parserObject$global:parserCounter').parserContentId$global:parserCounter]"; version = "[variables('parserObject$global:parserCounter').parserVersion$global:parserCounter]"; }; if($contentToImport.TemplateSpec) { # Adding the Parser respective TemplateSpec Resources and Active Parser and Metadata Resource addTemplateSpecParserResource $content $yaml $isyaml $contentResourceDetails } else { if ($global:parserCounter -eq 1 -and $(queryResourceExists) -and !$contentToImport.TemplateSpec) { $baseParserResource = [PSCustomObject] @{ type = "Microsoft.OperationalInsights/workspaces"; apiVersion = $contentResourceDetails.parserOperationalInsightsWorkspacesApiVersion; #"2020-08-01"; name = "[parameters('workspace')]"; location = "[parameters('workspace-location')]"; resources = @( ) } $global:baseMainTemplate.resources += $baseParserResource } $parserObj = [PSCustomObject] @{ type = "savedSearches"; apiVersion = $contentResourceDetails.savedSearchesApiVersion; #"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; } } $queryLocation = $(getQueryResourceLocation) if ($null -eq $queryLocation) { Write-Host "For Parser, getQueryResourceLocation function returned null for $file which is not valid" return; } else { $global:baseMainTemplate.resources[$queryLocation].resources += $parserObj } } $global:parserCounter += 1 }