This commit is contained in:
alexandre verkinderen 2021-08-31 15:24:02 +10:00
Родитель 0a174c29b9
Коммит b4b4a97667
5 изменённых файлов: 807 добавлений и 0 удалений

Просмотреть файл

@ -0,0 +1,746 @@
<#
THE SCRIPT IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SCRIPT OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
.SYNOPSIS
This PowerShell script integrates given Log Analytics Workspace tables data into Azure Data Explorer for long term retention.
For more information on how to use this script please visit: https://github.com/Azure/Azure-Sentinel/tree/master/Tools/AzureDataExplorer
.DESCRIPTION
It performs the following actions:
1. Queries the Log Analytics workspace tables.
2. Validates table names against data export supported tables AdxSupportedTables.json.
3. Creates target Table, Raw and Mapping in Azure Data Explorer.
4. Creates Event Hub namespaces (Standard) by dividing #1 tables by 10.
5. Creates data export rules via Azure REST API on Log Analytics workspace.
6. Creates data connection rules in Azure Data Explorer (ADX) database.
.PARAMETER SecretKey
Enter the SecretKey for the account that will be running this script
.PARAMETER LogAnalyticsSubscriptionId
Enter the Log Analytics subscription ID (required)
.PARAMETER LogAnalyticsWorkSpaceName
Enter the Log Analytics workspace name (required)
.PARAMETER LogAnalyticsResourceGroup
Enter the Resource Group name of Log Analytics workspace (required)
.PARAMETER AdxResourceGroup
Enter the Resource Group name of Azure Data Explorer (ADX) (required)
.PARAMETER AdxClusterURL
Enter the Resource Group name of Azure Data Explorer (ADX) Cluster URL (required)
.PARAMETER AdxDBName
Enter the Resource Group name of Azure Data Explorer (ADX) Database Name (required)
.PARAMETER UserInputTables
Comma separated list of Tables to export (optional)
.PARAMETER CreateDataConnection
Create data connection rules for each table with corresponding Event Hub topic, TableRaw and TableRawMapping (switch, default false)
.NOTES
Original AUTHOR: Sreedhar Ande
Modified to make it work with Pipelines: Alexandre Verkinderen
.EXAMPLE
.\Migrate-LA-to-ADX.ps1 -LogAnalyticsResourceGroup la-resgrp1 -LogAnalyticsWorkspaceName la-workspace-1 `
-AdxResourceGroup adx-resgrp1 -AdxClusterURL "https://adxcluster1.eastus2.kusto.windows.net" -AdxDBName AdxClusterDb1 -CreateDataConnection
#>
#region UserInputs
param(
[parameter(Mandatory = $true, HelpMessage = "Enter the subscriptionid for the Log Analytics workspace.")]
[string]$LogAnalyticsSubscriptionId,
[parameter(Mandatory = $true, HelpMessage = "Enter the user credential (key) to pass to Kusto CLI (User ID will be from Azure Context.Account.Id) ")]
[string]$SecretKey,
[parameter(Mandatory = $true, HelpMessage = "Enter the resource group location for the Log Analytics workspace.")]
[string]$LogAnalyticsResourceGroup,
[parameter(Mandatory = $true, HelpMessage = "Enter the Log Analytics workspace name from which to export data.")]
[string]$LogAnalyticsWorkspaceName,
[parameter(Mandatory = $true, HelpMessage = "Enter the resource group location for the existing Azure Data Explorer (ADX) cluster for which to export data.")]
[string]$AdxResourceGroup,
[parameter(Mandatory = $true, HelpMessage = "Enter the Azure Data Explorer (ADX) cluster URL.")]
[string]$AdxClusterURL,
[parameter(Mandatory = $true, HelpMessage = "Enter the Azure Data Explorer (ADX) cluster database name.")]
[string]$AdxDBName,
[parameter(Mandatory = $false, HelpMessage = "To use a specific set of tables (leave blank for all), Enter selected Log Analytics workspace table names separated by comma (,) (Case-Sensitive)")]
[string]$UserInputTables,
[parameter(Mandatory = $false, HelpMessage = "Create data connection rules for each table with corresponding Event Hub topic, TableRaw and TableRawMappings? (If Yes, the script will wait for 30 minutes, If No, you must create the data connection rules manually.)")]
[switch]$CreateDataConnection
)
#endregion UserInputs
#region StaticValues
[string]$AdxEngineUrl = "$AdxClusterURL/$AdxDBName"
[string]$KustoToolsPackage = "microsoft.azure.kusto.tools"
[string]$KustoConnectionString = "$AdxEngineUrl;Fed=True"
[string]$NuGetIndex = "https://api.nuget.org/v3/index.json"
[string]$NuGetDownloadUrl = "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe"
[string]$nugetPackageLocation = "$($env:USERPROFILE)\.nuget\packages"
#endregion StaticValues
#region HelperFunctions
function Write-Log {
<#
.DESCRIPTION
Write-Log is used to write information to a log file and to the console.
.PARAMETER Severity
parameter specifies the severity of the log message. Values can be: Information, Warning, or Error.
#>
[CmdletBinding()]
param(
[parameter()]
[ValidateNotNullOrEmpty()]
[string]$Message,
[string]$LogFileName,
[parameter()]
[ValidateNotNullOrEmpty()]
[ValidateSet('Information', 'Warning', 'Error')]
[string]$Severity = 'Information'
)
# Write the message out to the correct channel
switch ($Severity) {
"Information" { Write-Host $Message -ForegroundColor Green }
"Warning" { Write-Host $Message -ForegroundColor Yellow }
"Error" { Write-Host $Message -ForegroundColor Red }
}
try {
[PSCustomObject]@{
Time = (Get-Date -f g)
Message = $Message
Severity = $Severity
} | Export-Csv -Path "$PSScriptRoot\$LogFileName" -Append -NoTypeInformation -Force
}
catch {
Write-Error "An error occurred in Write-Log() method" -ErrorAction SilentlyContinue
}
}
function Get-RequiredModules {
<#
.DESCRIPTION
Get-Required is used to install and then import a specified PowerShell module.
.PARAMETER Module
parameter specifices the PowerShell module to install.
#>
[CmdletBinding()]
param (
[parameter(Mandatory = $true)] $Module
)
try {
$installedModule = Get-InstalledModule -Name $Module -ErrorAction SilentlyContinue
if ($null -eq $installedModule) {
Write-Log -Message "The $Module PowerShell module was not found" -LogFileName $LogFileName -Severity Warning
#check for Admin Privleges
$currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
if (-not ($currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator))) {
#Not an Admin, install to current user
Write-Log -Message "Can not install the $Module module. You are not running as Administrator" -LogFileName $LogFileName -Severity Warning
Write-Log -Message "Installing $Module module to current user Scope" -LogFileName $LogFileName -Severity Warning
Install-Module -Name $Module -Scope CurrentUser -Force
Import-Module -Name $Module -Force
}
else {
#Admin, install to all users
Write-Log -Message "Installing the $Module module to all users" -LogFileName $LogFileName -Severity Warning
Install-Module -Name $Module -Force -ErrorAction continue
Import-Module -Name $Module -Force -ErrorAction continue
}
}
# Install-Module will obtain the module from the gallery and install it on your local machine, making it available for use.
# Import-Module will bring the module and its functions into your current powershell session, if the module is installed.
}
catch {
Write-Log -Message "An error occurred in Get-RequiredModules() method" -LogFileName $LogFileName -Severity Error
exit
}
}
function Split-ArrayBySize {
[CmdletBinding()]
param (
[parameter(Mandatory = $true)] $AdxTabsArray,
[parameter(Mandatory = $true)] $ArraySize
)
try {
$slicedArraysResult = Split-Array -Item $AdxTabsArray -Size $ArraySize | ForEach-Object { '{0}' -f ($_ -join '","') }
return $slicedArraysResult
}
catch {
Write-Error "An error occurred in Split-ArrayBySize() method" -ErrorAction stop
exit
}
}
function Split-Array {
[CmdletBinding()]
param (
[parameter(Mandatory = $true)] [String[]]$Item,
[parameter(Mandatory = $true)] [int]$Size
)
begin { $Items = @() }
process {
foreach ($i in $Item ) { $Items += $i }
}
end {
0..[math]::Floor($Items.count / $Size) | ForEach-Object {
$x, $Items = $Items[0..($Size - 1)], $Items[$Size..$Items.Length]; , $x
}
}
}
function Start-SleepMessage {
<#
.DESCRIPTION
Start-SleepMessage is used to display a progress bar.
.PARAMETER Seconds
Specifices the path the the file that includes the commands to execute
.PARAMETER WaitMessage
Specifies the message to display with the progress bar.
#>
[CmdletBinding()]
param(
$Seconds,
$WaitMessage
)
$DoneDT = (Get-Date).AddSeconds($seconds)
while ($DoneDT -gt (Get-Date)) {
$SecondsLeft = $doneDT.Subtract((Get-Date)).TotalSeconds
$Percent = ($Seconds - $SecondsLeft) / $seconds * 100
Write-Progress -Activity $WaitMessage -Status "Please wait..." -SecondsRemaining $SecondsLeft -PercentComplete $Percent
[System.Threading.Thread]::Sleep(500)
}
Write-Progress -Activity $waitMessage -Status "Please wait..." -SecondsRemaining 0 -Completed
}
#endregion
#region MainFunctions
function Invoke-KustoCLI {
<#
.DESCRIPTION
Invoke-KustoCLI is used to execute the KustoCLI with the specified AdxCommandsFile.
.PARAMETER AdxCommandsFile
parameter specifices the path the the file that includes the commands to execute
#>
[CmdletBinding()]
param (
[parameter(Mandatory = $true)] $AdxCommandsFile
)
try {
$KustoToolsDir = "$env:USERPROFILE\.nuget\packages\$KustoToolsPackage\"
$CurrentDir = Get-Location
Set-Location $ScriptDir
if (!(Test-Path $KustoToolsDir)) {
if (!(Test-Path nuget)) {
Write-Log -Message "The NuGet module is not found" -LogFileName $LogFileName -Severity Warning
Write-Log -Message "Downloading NuGet package" -LogFileName $LogFileName -Severity Information
(New-Object net.webclient).downloadFile($NuGetDownloadUrl, "$pwd\nuget.exe")
}
Write-Log -Message "Installing Kusto Tools Package" -LogFileName $LogFileName -Severity Information
&.\nuget.exe install $kustoToolsPackage -Source $nugetIndex -OutputDirectory $nugetPackageLocation
}
$KustoExe = $KustoToolsDir + @(Get-ChildItem -Recurse -Path $KustoToolsDir -Name kusto.cli.exe)[-1]
if (!(Test-Path $KustoExe)) {
Write-Log -Message "Unable to find Kusto client tool $KustoExe. exiting" -LogFileName $LogFileName -Severity Warning
Write-Warning "Unable to find Kusto client tool $KustoExe. exiting"
return
}
Write-Log -Message "Executing queries on Azure Data Explorer (ADX)" -LogFileName $LogFileName -Severity Information
Invoke-Expression "$kustoExe `"$kustoConnectionString`" -script:`"$adxCommandsFile`""
Set-Location $CurrentDir
}
catch {
Write-Log -Message "An error occurred in Invoke-KustoCLI() method" -LogFileName $LogFileName -Severity Error
exit
}
}
function New-AdxRawMappingTables {
<#
.DESCRIPTION
New-AdxRawMappingTables is used to create raw mapping tables
.PARAMETER LaTables
Parameter specifices the Log Analytics tables to create
#>
[CmdletBinding()]
param (
[parameter(Mandatory = $true)] $LaTables,
[parameter(Mandatory = $true)] $LaMappingDecision
)
if (!(Test-Path "$PSScriptRoot\KustoQueries" -PathType Container)) {
New-Item -Path $PSScriptRoot -Name "KustoQueries" -ItemType "directory"
}
if (Test-Path "$PSScriptRoot\ADXSupportedTables.json") {
$supportedTables = Get-Content "$PSScriptRoot\ADXSupportedTables.json" | ConvertFrom-Json
}
else {
Write-Log " Unable to load $($PSScriptRoot)\ADXSupportedTables.json" -Severity Error
exit
}
foreach ($table in $LaTables) {
if ($LaMappingDecision) {
$TableName = $table.'$table'
}
else {
$TableName = $table
}
if ($TableName -match '_CL$') {
Write-Log -Message "Custom log table : $TableName not supported" -LogFileName $LogFileName -Severity Information
}
elseif ($supportedTables.SupportedTables -ccontains $TableName.ToString().Trim()) {
Write-Log -Message "Retrieving schema and mappings for $TableName" -LogFileName $LogFileName -Severity Information
$query = $TableName + ' | getschema | project ColumnName, DataType'
$AdxTablesArray.Add($TableName.Trim())
Write-Verbose "Executing: (Invoke-AzOperationalInsightsQuery -WorkspaceId $LogAnalyticsWorkspaceId -Query $query).Results"
$output = (Invoke-AzOperationalInsightsQuery -WorkspaceId $LogAnalyticsWorkspaceId -Query $query -DefaultProfile $Context).Results
$TableExpandFunction = $TableName + 'Expand'
$TableRaw = $TableName + 'Raw'
$RawMapping = $TableRaw + 'Mapping'
$FirstCommand = @()
$ThirdCommand = @()
foreach ($record in $output) {
if ($record.DataType -eq 'System.DateTime') {
$dataType = 'datetime'
$ThirdCommand += $record.ColumnName + " = todatetime(events." + $record.ColumnName + "),"
}
else {
$dataType = 'string'
$ThirdCommand += $record.ColumnName + " = tostring(events." + $record.ColumnName + "),"
}
$FirstCommand += $record.ColumnName + ":" + "$dataType" + ","
}
$schema = ($FirstCommand -join '') -replace ',$'
$function = ($ThirdCommand -join '') -replace ',$'
$CreateRawTable = '.create table {0} (Records:dynamic)' -f $TableRaw
$CreateRawMapping = @'
.create table {0} ingestion json mapping '{1}' '[{{"column":"Records","Properties":{{"path":"$.records"}}}}]'
'@ -f $TableRaw, $RawMapping
$CreateRetention = '.alter-merge table {0} policy retention softdelete = 0d' -f $TableRaw
$CreateTable = '.create table {0} ({1})' -f $TableName, $schema
$CreateFunction = @'
.create-or-alter function {0} {{{1} | mv-expand events = Records | project {2} }}
'@ -f $TableExpandFunction, $TableRaw, $function
$CreatePolicyUpdate = @'
.alter table {0} policy update @'[{{"Source": "{1}", "Query": "{2}()", "IsEnabled": "True", "IsTransactional": true}}]'
'@ -f $TableName, $TableRaw, $TableExpandFunction
$scriptDir = "$PSScriptRoot\KustoQueries"
New-Item "$scriptDir\adxCommands.txt"
Add-Content "$scriptDir\adxCommands.txt" "`n$CreateRawTable"
Add-Content "$scriptDir\adxCommands.txt" "`n$CreateRawMapping"
Add-Content "$scriptDir\adxCommands.txt" "`n$CreateRetention"
Add-Content "$scriptDir\adxCommands.txt" "`n$CreateTable"
Add-Content "$scriptDir\adxCommands.txt" "`n$CreateFunction"
Add-Content "$scriptDir\adxCommands.txt" "`n$CreatePolicyUpdate"
try {
Invoke-KustoCLI -AdxCommandsFile "$scriptDir\adxCommands.txt"
Remove-Item $ScriptDir\adxCommands.txt -Force -ErrorAction Ignore
}
catch {
Write-Log -Message "An error occurred in New-AdxRawMappingTables() method" -LogFileName $LogFileName -Severity Error
exit
}
Write-Log -Message "Successfully created Raw and Mapping tables for: $TableName in ADX cluster database." -LogFileName $LogFileName -Severity Information
}
else {
Write-Log -Message "$TableName table is not supported by data export." -LogFileName $LogFileName -Severity Error
}
}
}
function New-EventHubNamespace {
<#
.DESCRIPTION
New-EventHubNamespace is used to create an Event Hub namespace.
.PARAMETER ArraysObject
Parameter specifices the Event Hub namespace.
#>
[CmdletBinding()]
param (
[parameter(Mandatory = $true)] $ArraysObject
)
try {
$EventHubsArray = @()
foreach ($slicedArray in $ArraysObject) {
if ($slicedArray.Length -gt 0) {
#Create Event Hub NameSpace
$randomNumber = Get-Random
$EventHubNamespaceName = "$($LogAnalyticsWorkspaceName)-$($randomNumber)"
$EventHubsArray += $EventHubNamespaceName
Write-Verbose "Executing: New-AzEventHubNamespace -ResourceGroupName $LogAnalyticsResourceGroup -NamespaceName $EventHubNamespaceName `
-Location $LogAnalyticsLocation -SkuName Standard -SkuCapacity 12 -EnableAutoInflate -MaximumThroughputUnits 20"
try {
Write-Log -Message "Create a new Event Hub Namespace:$EventHubNamespaceName in resource group:$LogAnalyticsResourceGroup" -LogFileName $LogFileName -Severity Information
Set-Item Env:\SuppressAzurePowerShellBreakingChangeWarnings "true"
$ResultEventHubNS = New-AzEventHubNamespace -ResourceGroupName $LogAnalyticsResourceGroup `
-NamespaceName $EventHubNamespaceName `
-Location $LogAnalyticsLocation `
-SkuName "Standard" `
-SkuCapacity 12 `
-EnableAutoInflate `
-MaximumThroughputUnits 20
if ($ResultEventHubNS.ProvisioningState.Trim().ToLower() -eq "succeeded") {
Write-Log -Message "$EventHubNamespaceName created successfully" -LogFileName $LogFileName -Severity Information
}
}
catch {
Write-Log -Message "$($_.Exception.Response.StatusCode.value__)" -LogFileName $LogFileName -Severity Error
Write-Log -Message "$($_.Exception.Response.StatusDescription)" -LogFileName $LogFileName -Severity Error
}
}
}
return $EventHubsArray
}
catch {
Write-Log -Message "An error occurred in Create-EventHubNamespace() method" -LogFileName $LogFileName -Severity Error
exit
}
}
function New-LaDataExportRule {
<#
.DESCRIPTION
New-LaDataExportRule is used to create the Log Analytics export rule,
.PARAMETER AdxEventHubs
Parameter specifices Azure Data Explorer Event Hub to create export rule.
.PARAMETER TablesArrayCollection
Parameter specifies the table names used to create the export rule.
#>
[CmdletBinding()]
param (
[Parameter(Mandatory = $true, Position = 0)] $AdxEventHubs,
[Parameter(Mandatory = $true, Position = 1)] $TablesArrayCollection
)
Write-Log -Message "Creating Log Analytics data export rules" -LogFileName $LogFileName -Severity Information
try {
$Count = 0
foreach ($AdxEventHub in $AdxEventHubs) {
$EventHubNameSpace = Get-AzEventHubNamespace -ResourceGroupName $LogAnalyticsResourceGroup -NamespaceName $AdxEventHub
if ($AdxEventHubs.Count -gt 1) {
$ExportRuleTables = '"{0}"' -f ($TablesArrayCollection[$count] -join '","')
}
else {
$ExportRuleTables = '"{0}"' -f ($TablesArrayCollection -join '","')
}
if ($EventHubNameSpace.ProvisioningState -eq "Succeeded") {
$RandomNumber = Get-Random
$LaDataExportRuleName = "$($LogAnalyticsWorkspaceName)-$($RandomNumber)"
$DataExportAPI = "https://management.azure.com/subscriptions/$SubscriptionId/resourcegroups/$LogAnalyticsResourceGroup/providers/Microsoft.operationalInsights/workspaces/$LogAnalyticsWorkspaceName/dataexports/$laDataExportRuleName" + "?api-version=2020-08-01"
$AzureAccessToken = (Get-AzAccessToken).Token
$LaAPIHeaders = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
$LaAPIHeaders.Add("Content-Type", "application/json")
$LaAPIHeaders.Add("Authorization", "Bearer $AzureAccessToken")
$DataExportBody = @"
{
"properties": {
"destination": {
"resourceId": "$($EventHubNameSpace.Id)"
},
"tablenames": [$ExportRuleTables],
"enable": true
}
}
"@
Write-Verbose "Executing: Invoke-RestMethod -Uri $DataExportAPI -Method 'PUT' -Headers $LaAPIHeaders -Body $DataExportBody"
try {
$CreateDataExportRule = Invoke-RestMethod -Uri $DataExportAPI -Method "PUT" -Headers $LaAPIHeaders -Body $DataExportBody
Write-Log -Message $CreateDataExportRule -LogFileName $LogFileName -Severity Information
}
catch {
Write-Log -Message $($_.Exception.Response.StatusCode.value__) -LogFileName $LogFileName -Severity Error
Write-Log -Message $($_.Exception.Response.StatusDescription) -LogFileName $LogFileName -Severity Error
}
$Count++
}
else {
Start-SleepMessage 300
}
}
}
catch {
Write-Error "An error occurred in Create-LaDataExportRule() method" -ErrorAction stop
exit
}
}
function New-ADXDataConnectionRules {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true, Position = 0)] $AdxEventHubs
)
try {
Register-AzResourceProvider -ProviderNamespace Microsoft.Kusto
Write-Log -Message "Creating Azure Data Explorer data connection" -LogFileName $LogFileName -Severity Information
$ADXClusterName = $ADXClusterURL.split('.')[0].replace("https://", "").Trim()
foreach ($AdxEH in $AdxEventHubs) {
Write-Verbose "Executing: Get-AzEventHub -ResourceGroup $LogAnalyticsResourceGroup -NamespaceName $AdxEH"
try {
$EventHubTopics = Get-AzEventHub -ResourceGroup $LogAnalyticsResourceGroup -NamespaceName $AdxEH
if ($null -ne $EventHubTopics) {
foreach ($EventHubTopic in $EventHubTopics) {
$TableEventHubTopic = $EventHubTopic.Name.split('-')[1]
# The above statement will return Table name in lower case
# Azure Kusto Data connection is expecting the table name in title case (Case Sensitive)
# In order to get exact same case table name, getting it from Source array
$AdxTables = $AdxTablesArray.ToArray()
$ArrIndex = $AdxTables.ForEach{ $_.ToLower() }.IndexOf($tableEventHubTopic)
$EventHubResourceId = $EventHubTopic.Id
$AdxTableRealName = $AdxTables[$ArrIndex].Trim().ToString()
$AdxTableRaw = "$($AdxTableRealName)Raw"
$AdxTableRawMapping = "$($AdxTableRealName)RawMapping"
$DataConnName = "dc-$($TableEventHubTopic)"
$DataConnAPI = "https://management.azure.com/subscriptions/$SubscriptionId/resourceGroups/$ADXResourceGroup/providers/Microsoft.Kusto/clusters/$ADXClusterName/databases/$ADXDBName/dataConnections/$dataConnName" + "?api-version=2021-01-01"
$AzureAccessToken = (Get-AzAccessToken).Token
$DataConnAPIHeaders = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
$DataConnAPIHeaders.Add("Content-Type", "application/json")
$DataConnAPIHeaders.Add("Authorization", "Bearer $AzureAccessToken")
$DataConnBody = @"
{
"location": "$LogAnalyticsLocation",
"kind": "EventHub",
"properties": {
"eventHubResourceId": "$EventHubResourceId",
"consumerGroup": "$('$Default')",
"dataFormat":"JSON",
"tableName":"$AdxTableRaw",
"mappingRuleName":"$AdxTableRawMapping",
"compression":"None"
}
}
"@
Write-Verbose "Executing: Invoke-RestMethod -Uri $DataConnAPI -Method 'PUT' -Headers $LaAPIHeaders -Body $DataConnBody"
try {
$CreateDataConnRule = Invoke-RestMethod -Uri $DataConnAPI -Method "PUT" -Headers $DataConnAPIHeaders -Body $DataConnBody
Write-Log -Message $CreateDataConnRule -LogFileName $LogFileName -Severity Information
}
catch {
Write-Log -Message "An error occurred in creating data connection for $($eventHubTopic.Name)" -LogFileName $LogFileName -Severity Error
Write-Log -Message $($_.Exception.Response.StatusCode.value__) -LogFileName $LogFileName -Severity Error
Write-Log -Message $($_.Exception.Response.StatusDescription) -LogFileName $LogFileName -Severity Error
}
}
}
else {
Write-Log -Message "Event Hub topics not available in $AdxEH" -LogFileName $LogFileName -Severity Error
}
}
catch {
Write-Log -Message "An error occurred in retrieving Event Hub topics from $AdxEH" -LogFileName $LogFileName -Severity Error
}
}
}
catch {
Write-Log -Message "An error occurred in Create-ADXDataConnectionRules() method" -LogFileName $LogFileName -Severity Error
exit
}
}
#endregion
#region DriverProgram
Get-RequiredModules("Az.Resources")
Get-RequiredModules("Az.OperationalInsights")
# Check Powershell version, needs to be 5 or higher
if ($host.Version.Major -lt 5) {
Write-Log "Supported PowerShell version for this script is 5 or above" -LogFileName $LogFileName -Severity Error
exit
}
$TimeStamp = Get-Date -Format yyyyMMdd_HHmmss
$LogFileName = '{0}_{1}.csv' -f "ADXMigration", $TimeStamp
Write-Output "`n`n Starting Migrate-LA-to-Adx.ps1 at: $(Get-Date)"
Write-Output " Creating log $LogFileName"
Write-Host "`n`n`r`If not already authenticated, you will be prompted to sign in to Azure.`nMake sure that your credentials have:" -BackgroundColor Blue
Write-Host "`n > Azure Log Analytics workspace 'Read' permissions on: $($LogAnalyticsWorkspaceName)`n > Azure Data Explorer Database 'User' permissions on: $($AdxDBName). `n`nThese permissions are required for the script to read the Log Analytics workspace tables and to create tables in Azure Data Explorer.`r`n" -BackgroundColor Blue
Set-AzContext -Subscription $LogAnalyticsSubscriptionId
$Context = Get-AzContext
if (!$Context) {
Write-Output "Context was not set. Connecting with Connect-AzAccount"
Connect-AzAccount -SubscriptionId $LogAnalyticsSubscriptionId
$Context = Get-AzContext
}
$SubscriptionId = $LogAnalyticsSubscriptionId
$KustoConnectionString = $KustoConnectionString + ";Authority Id=$($Context.tenant.id);AppClientId=$($Context.Account.id);AppKey=$SecretKey"
Write-Verbose "Executing: Get-AzOperationalInsightsWorkspace -Name $LogAnalyticsWorkspaceName -ResourceGroupName $LogAnalyticsResourceGroup -DefaultProfile $context"
try {
$WorkspaceObject = Get-AzOperationalInsightsWorkspace -Name $LogAnalyticsWorkspaceName -ResourceGroupName $LogAnalyticsResourceGroup -DefaultProfile $Context
$LogAnalyticsLocation = $WorkspaceObject.Location
$LogAnalyticsWorkspaceId = $WorkspaceObject.CustomerId
if ($null -ne $LogAnalyticsWorkspaceId) {
Write-Log -Message "Workspace named $LogAnalyticsWorkspaceName in region $LogAnalyticsLocation exists." -LogFileName $LogFileName -Severity Information
}
else {
Write-Log -Message "$LogAnalyticsWorkspaceName not found" -LogFileName $LogFileName -Severity Error
}
}
catch {
Write-Log -Message "Error occurred in retreiving Log Analytics workspace: $LogAnalyticsWorkspaceName" -LogFileName $LogFileName -Severity Error
}
#region ADXTableCreation
if ([string]::IsNullOrEmpty($UserInputTables)){
$LaTablesQuestionDecision = $true
}
else {
$LaTablesQuestionDecision = $false
}
if ($LaTablesQuestionDecision) {
Write-Verbose "Executing: Invoke-AzOperationalInsightsQuery -WorkspaceId $LogAnalyticsWorkspaceId -Query $QueryAllTables"
try {
Write-Log -Message "Retrieving tables from $LogAnalyticsWorkspaceName" -LogFileName $LogFileName -Severity Information
$QueryAllTables = 'search *| distinct $table| sort by $table asc nulls last'
$ResultsAllTables = (Invoke-AzOperationalInsightsQuery -WorkspaceId $LogAnalyticsWorkspaceId -Query $QueryAllTables -DefaultProfile $Context).Results
}
catch {
Write-Log -Message "An error occurred in querying table names from $LogAnalyticsWorkspaceName" -LogFileName $LogFileName -Severity Error
exit
}
}
else {
try {
Write-Host "`nEnter selected Log Analytics workspace table names separated by comma (,) (Case-Sensitive)" -ForegroundColor Blue
$ResultsAllTables = $UserInputTables.Split(',')
}
catch {
Write-Log -Message "Incorrect user input! Table names must be separated by comma (,)" -LogFileName $LogFileName -Severity Error
exit
}
}
$AdxTablesArray = New-Object System.Collections.Generic.List[System.Object]
New-AdxRawMappingTables -LaTables $ResultsAllTables -LaMappingDecision $LaTablesQuestionDecision
#endregion
#region EventHubsCreation
Write-Verbose " There are $($AdxTablesArray.ToArray().Count) supported tables to map."
if ($AdxTablesArray.ToArray().Count -gt 0) {
$AdxMappedTables = Split-ArrayBySize -AdxTabsArray $AdxTablesArray.ToArray() -ArraySize 10
Write-Verbose "Executing: New-EventHubNamespace -ArraysObject $AdxMappedTables"
$EventHubsForADX = New-EventHubNamespace -ArraysObject $AdxMappedTables
}
else {
Write-Log "There are $($AdxTablesArray.ToArray().Count) supported tables to map in $($LogAnalyticsWorkspaceName), you must choose a workspace with at least one supported table." -LogFileName $LogFileName -Severity Error
exit
}
#endregion
#region LogAnalyticsDataExportRule
New-LaDataExportRule -AdxEventHubs $EventHubsForADX -TablesArrayCollection $AdxMappedTables
#endregion
#region ADXDataConnectionRule
if ($CreateDataConnection) {
Start-SleepMessage -Seconds 1800 -waitMessage "Provisioning Event Hub topics for Log Analytics tables"
New-ADXDataConnectionRules -AdxEventHubs $EventHubsForADX
}
else {
Write-Log -Message "Please manually create data connection rules for $AdxDBName in $AdxEngineUrl" -LogFileName $LogFileName -Severity Warning
}
#endregion

Просмотреть файл

@ -0,0 +1,38 @@
# Integrate Azure Data Explorer (ADX) for long-term log retention with a Azure DevOps Pipeline
**Author: Alexandre Verkinderen**
As part of our Azure Enterprise Scale deployment <https://github.com/Azure/Enterprise-Scale/> we also provision Log Analytics and Sentinel and have a need to export this data for archviving purpose. The excellent script provided by Sreedhar Ande <https://github.com/Azure/Azure-Sentinel/blob/master/Tools/AzureDataExplorer/Migrate-LA-to-ADX.ps1> was the missing piece to be able automate the export of the Log Analtyics logs.
Our Enterprise Scale deployment is fully automated and repeatable accross different environments using Azure DevOps Pipelines. This script is slightly modified from the original one and is intented to be executed from within an Azure DevOps Pipeline to configure Log Analtyics workspace to an Eventhubs and setup ADX connectors for ingestion. The provided yaml pipeline is just an example and can be modified to your requirements.
## Prerequisites
- Make sure you have all the prerequisites in place as described here <https://github.com/Azure/Azure-Sentinel/tree/master/Tools/AzureDataExplorer#prerequisites>.
- Make sure that the SPN you use for the Service Connection within your Pipeline has permissions as mentioned above
- Get the SPN secret from your SPN
- Create a variable group (we call it ES-General) with the following variables and fill in the values with your Azure Resources
- ARM_CLIENT_SECRET (secret of the appID)
- LogAnalyticsSubscriptionId
- LogAnalyticsWorkSpaceName
- LogAnalyticsResourceGroup
- AdxResourceGroup
- AdxClusterURL
- AdxDBName
- LAExportTablesToADX (specify tables you want to export like AzureActivity,SecurityRecommendation)
![VariableGroup](../images/VariableGroup.PNG)
## Script
The script requires one extra parameter compared to the original script: "SecretKey". This is because the kustocli.exe is prompting for authentication. <https://docs.microsoft.com/en-us/azure/data-explorer/kusto/api/connection-strings/kusto#aad-based-authentication-examples>. As we are using an SPN to connect to Azure we have to pass the ApplicationClientId and ApplicationKey.
`$KustoConnectionString = $KustoConnectionString + ";Authority Id=$($Context.tenant.id);AppClientId=$($Context.Account.id);AppKey=$SecretKey"`
## Pipeline
Import the provided yaml pipeline in your Azur DevOps environment and change the ScriptPath parameter to match the location where you stored the script in your repository. Once done execute the script.
![Pipeline](../images/Pipeline.PNG)
The script will take roughly 30 minutes to execute.

Просмотреть файл

@ -0,0 +1,23 @@
# Pipeline Policies
name: $(BuildDefinitionName)_$(SourceBranchName)_$(Date:yyyyMMdd)$(Rev:.r)
trigger: none
pool:
vmImage: 'windows-latest'
variables:
- group: ES-General
jobs:
- job: MigrateLAtoADX
condition: true
displayName: 'Migrate LA to ADX'
steps:
- task: AzurePowerShell@5
displayName: 'Migrate LA to ADX'
inputs:
azureSubscription: 'Root Management Group'
ScriptType: 'FilePath'
ScriptPath: 'Code/Scripts/adx/Migrate-LA-to-ADX.ps1'
ScriptArguments: '-SecretKey $(ARM_CLIENT_SECRET) -LogAnalyticsSubscriptionId $(LogAnalyticsSubscriptionId) -LogAnalyticsWorkSpaceName $(LogAnalyticsWorkSpaceName) -LogAnalyticsResourceGroup $(LogAnalyticsResourceGroup) -AdxResourceGroup $(AdxResourceGroup) -AdxClusterURL $(AdxClusterURL) -AdxDBName $(AdxDBName) -UserInputTables "$(LAExportTablesToADX)" -CreateDataConnection'
azurePowerShellVersion: 'LatestVersion'

Двоичные данные
Tools/AzureDataExplorer/images/Pipeline.PNG Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 83 KiB

Двоичные данные
Tools/AzureDataExplorer/images/VariableGroup.PNG Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 22 KiB