<# .SYNOPSIS script to query kusto with AAD authorization or token using kusto rest api script gives ability to import, export, execute query and commands, and removing empty columns .DESCRIPTION this script will setup Microsoft.IdentityModel.Clients Msal for use with powershell 5.1, 6, and 7 KustoObj will be created as $global:kusto to hold properties and run methods from use the following to save and pass arguments: [net.servicePointManager]::Expect100Continue = $true;[net.servicePointManager]::SecurityProtocol = [net.SecurityProtocolType]::Tls12; invoke-webRequest "https://raw.githubusercontent.com/jagilber/powershellScripts/master/kusto-rest.ps1" -outFile "$pwd/kusto-rest.ps1"; .\kusto-rest.ps1 -cluster %kusto cluster% -database %kusto database% .NOTES Author : jagilber File Name : kusto-rest.ps1 Version : 231030 History : migrate from netcoreapp2.1 to net6.0 .EXAMPLE .\kusto-rest.ps1 -cluster kustocluster -database kustodatabase .EXAMPLE .\kusto-rest.ps1 -cluster kustocluster -database kustodatabase $kusto.Exec('.show tables') .EXAMPLE $kusto.viewresults = $true $kusto.SetTable($table) $kusto.SetDatabase($database) $kusto.SetCluster($cluster) $kusto.parameters = @{'T'= $table} $kusto.ExecScript("..\KustoFunctions\sflogs\TraceKnownIssueSummary.csl", $kusto.parameters) .EXAMPLE $kusto.SetTable("test_$env:USERNAME").Import() .EXAMPLE $kusto.SetPipe($true).SetCluster('azure').SetDatabase('azure').Exec("EventEtwTable | where TIMESTAMP > ago(1d) | where TenantName == $tenantId") .EXAMPLE .\kusto-rest.ps1 -cluster kustocluster -database kustodatabase $kusto.Exec('.show tables') $kusto.ExportCsv("$env:temp\test.csv") type $env:temp\test.csv .PARAMETER query query string or command to execute against a kusto database .PARAMETER cluster [string]kusto cluster name. (host name not fqdn) example: kustocluster example: azurekusto.eastus .PARAMETER database [string]kusto database name .PARAMETER table [string]optional kusto table for import .PARAMETER headers [string]optional kusto table headers for import ['columnname']:columntype, .PARAMETER resultFile [string]optional json file name and path to store raw result content .PARAMETER viewResults [bool]option if enabled will display results in console output .PARAMETER token [string]optional token to connect to kusto. if not provided, script will attempt to authorize user to given cluster and database .PARAMETER limit [int]optional result limit. default 10,000 .PARAMETER script [string]optional path and name of kusto script file (.csl|.kusto) to execute .PARAMETER clientSecret [string]optional azure client secret to connect to kusto. if not provided, script will attempt to authorize user to given cluster and database requires clientId .PARAMETER clientId [string]optional azure client id to connect to kusto. if not provided, script will attempt to authorize user to given cluster and database requires clientSecret .PARAMETER tenantId [guid]optional tenantId to use for authorization. default is 'common' .PARAMETER force [bool]enable to force authentication regardless if token is valid .PARAMETER serverTimeout [timespan]optional override default 4 minute kusto server side timeout. max 60 minutes. .PARAMETER updateScript [switch]optional enable to download latest version of script .PARAMETER parameters [hashtable]optional hashtable of parameters to pass to kusto script (.csl|kusto) file .OUTPUTS KustoObj .LINK https://raw.githubusercontent.com/jagilber/powershellScripts/master/kusto-rest.ps1 #> [cmdletbinding()] param( [string]$cluster, [string]$database, [string]$query = '.show tables', [bool]$fixDuplicateColumns, [bool]$removeEmptyColumns = $true, [string]$table, [string]$headers, [string]$identityPackageLocation, [string]$resultFile, # = ".\result.json", [bool]$createResults = $true, [bool]$viewResults = $true, [string]$token, [int]$limit, [string]$script, [string]$clientSecret, [string]$clientId = "1950a258-227b-4e31-a9cf-717495945fc2", [string]$tenantId = "common", [bool]$pipeLine, [string]$redirectUri = "http://localhost", # "urn:ietf:wg:oauth:2.0:oob", #$null [bool]$force, [timespan]$serverTimeout = (new-Object timespan (0, 4, 0)), [switch]$updateScript, [hashtable]$parameters = @{ } #@{'clusterName' = $resourceGroup; 'dnsName' = $resourceGroup;} ) $PSModuleAutoLoadingPreference = 2 $ErrorActionPreference = "continue" $global:kusto = $null $global:identityPackageLocation $packageVersion = "4.28.0" if ($updateScript) { invoke-webRequest "https://raw.githubusercontent.com/jagilber/powershellScripts/master/kusto-rest.ps1" -outFile "$psscriptroot/kusto-rest.ps1"; write-warning "script updated. restart script" return } function AddIdentityPackageType([string]$packageName, [string] $edition) { # support ps core on linux if ($IsLinux) { $env:USERPROFILE = $env:HOME } [string]$nugetPackageDirectory = "$($env:USERPROFILE)/.nuget/packages" [string]$nugetSource = "https://api.nuget.org/v3/index.json" [string]$nugetDownloadUrl = "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe" [io.directory]::createDirectory($nugetPackageDirectory) [string]$packageDirectory = "$nugetPackageDirectory/$packageName" $global:identityPackageLocation = get-identityPackageLocation $packageDirectory if (!$global:identityPackageLocation) { if ($psedition -ieq 'core') { $tempProjectFile = './temp.csproj' #dotnet new console $csproj = " Exe $edition " out-file -InputObject $csproj -FilePath $tempProjectFile write-host "dotnet restore --packages $packageDirectory --no-cache --no-dependencies $tempProjectFile" dotnet restore --packages $packageDirectory --no-cache --no-dependencies $tempProjectFile remove-item "$pwd/obj" -re -fo remove-item -path $tempProjectFile } else { $nuget = "nuget.exe" if (!(test-path $nuget)) { $nuget = "$env:temp/nuget.exe" if (!(test-path $nuget)) { [net.webclient]::new().DownloadFile($nugetDownloadUrl, $nuget) } } [string]$localPackages = . $nuget list -Source $nugetPackageDirectory if ($force -or !($localPackages -imatch "$edition.$packageName")) { write-host "$nuget install $packageName -Source $nugetSource -outputdirectory $nugetPackageDirectory -verbosity detailed" . $nuget install $packageName -Source $nugetSource -outputdirectory $nugetPackageDirectory -verbosity detailed } else { write-host "$packageName already installed" -ForegroundColor green } } } $global:identityPackageLocation = get-identityPackageLocation $packageDirectory write-host "identityDll: $($global:identityPackageLocation)" -ForegroundColor Green add-type -literalPath $global:identityPackageLocation return $true } function get-identityPackageLocation($packageDirectory) { $pv = [version]::new($packageVersion) $pv = [version]::new($pv.Major, $pv.Minor) $versions = @{} $files = @(get-childitem -Path $packageDirectory -Recurse | where-object FullName -imatch "lib.$edition.$packageName\.dll") write-host "existing identity dlls $($files|out-string)" foreach ($file in @($files.fullname)) { $versionString = [regex]::match($file, ".$packageName.([0-9.]+?).lib.$edition", [text.regularexpressions.regexoptions]::IgnoreCase).Groups[1].Value if (!$versionString) { continue } $version = [version]::new($versionString) [void]$versions.add($file, [version]::new($version.Major, $version.Minor)) } foreach ($version in $versions.GetEnumerator()) { write-host "comparing file version:$($version.value) to configured version:$($pv)" if ($version.value -ge $pv) { return $version.Key } } return $null } function get-msalLibrary() { # Install latest AD client library try { if (([Microsoft.Identity.Client.ConfidentialClientApplication]) -and !$force) { write-host "[Microsoft.Identity.Client.AzureCloudInstance] already loaded. skipping" -ForegroundColor Cyan return } } catch { write-verbose "exception checking for identity client:$($error|out-string)" $error.Clear() } if ($global:PSVersionTable.PSEdition -eq "Core") { write-host "setting up microsoft.identity.client for .net core" if (!(AddIdentityPackageType -packageName "Microsoft.Identity.Client" -edition "net6.0")) { write-error "unable to add package" return $false } if (!(AddIdentityPackageType -packageName "Microsoft.IdentityModel.Abstractions" -edition "net6.0")) { write-error "unable to add package" return $false } } else { write-host "setting up microsoft.identity.client for .net framework" if (!(AddIdentityPackageType -packageName "Microsoft.Identity.Client" -edition "net461")) { write-error "unable to add package" return $false } if (!(AddIdentityPackageType -packageName "Microsoft.IdentityModel.Abstractions" -edition "net6.0")) { write-error "unable to add package" return $false } } } get-msalLibrary # comment next line after microsoft.identity.client type has been imported into powershell session to troubleshoot 1 of 2 invoke-expression @' class KustoObj { hidden [string]$identityPackageLocation = $identityPackageLocation hidden [object]$authenticationResult hidden [Microsoft.Identity.Client.ConfidentialClientApplication] $confidentialClientApplication = $null [string]$clientId = $clientId hidden [string]$clientSecret = $clientSecret [string]$Cluster = $cluster [string]$Database = $database [bool]$FixDuplicateColumns = $fixDuplicateColumns [bool]$Force = $force [string]$Headers = $headers [int]$Limit = $limit [hashtable]$parameters = $parameters hidden [Microsoft.Identity.Client.PublicClientApplication] $publicClientApplication = $null [bool]$PipeLine = $null [string]$Query = $query hidden [string]$redirectUri = $redirectUri [bool]$RemoveEmptyColumns = $removeEmptyColumns hidden [object]$Result = $null [object]$ResultObject = $null [object]$ResultTable = $null [string]$ResultFile = $resultFile [string]$Script = $script [string]$Table = $table [string]$tenantId = $tenantId [timespan]$ServerTimeout = $serverTimeout hidden [string]$token = $token [bool]$CreateResults = $createResults [bool]$ViewResults = $viewResults [hashtable]$Tables = @{} [hashtable]$Functions = @{} hidden [hashtable]$FunctionObjs = @{} KustoObj() { } static KustoObj() { } [void] ClearResults() { $this.ResultObject = $null $this.ResultTable = $null } [KustoObj] CreateResultTable() { $this.ResultTable = [collections.arraylist]@() $columns = @{ } if (!$this.ResultObject.tables) { write-warning "run query first" return $this.Pipe() } foreach ($column in ($this.ResultObject.tables[0].columns)) { try { [void]$columns.Add($column.ColumnName, $null) } catch { write-warning "$($column.ColumnName) already added" } } $resultModel = New-Object -TypeName PsObject -Property $columns $rowCount = 0 foreach ($row in ($this.ResultObject.tables[0].rows)) { $count = 0 $resultCopy = $resultModel.PsObject.Copy() foreach ($column in ($this.ResultObject.tables[0].columns)) { #write-verbose "createResultTable: procesing column $count" $resultCopy.($column.ColumnName) = $row[$count++] } write-verbose "createResultTable: processing row $rowCount columns $count" $rowCount++ [void]$this.ResultTable.add($resultCopy) } $this.ResultTable = $this.RemoveEmptyResults($this.ResultTable) return $this.Pipe() } [KustoObj] Exec([string]$query) { $this.Query = $query $this.Exec() $this.Query = $null return $this.Pipe() } [KustoObj] Exec() { $startTime = get-date $this if (!$this.Limit) { $this.Limit = 10000 } if (!$this.Script -and !$this.Query) { Write-Warning "-script and / or -query should be set. exiting" return $this.Pipe() } if (!$this.Cluster -or !$this.Database) { Write-Warning "-cluster and -database have to be set once. exiting" return $this.Pipe() } if ($this.Query) { write-host "table:$($this.Table) query:$($this.Query.substring(0, [math]::min($this.Query.length,512)))" -ForegroundColor Cyan } if ($this.Script) { write-host "script:$($this.Script)" -ForegroundColor Cyan } if ($this.Table -and $this.Query.startswith("|")) { $this.Query = $this.Table + $this.Query } $this.ResultObject = $this.Post($null) if ($this.ResultObject.Exceptions) { write-warning ($this.ResultObject.Exceptions | out-string) $this.ResultObject.Exceptions = $null } if ($this.ViewResults -or $this.CreateResults) { $this.CreateResultTable() if ($this.ViewResults) { write-host ($this.ResultTable | out-string) } } if ($this.ResultFile) { out-file -FilePath $this.ResultFile -InputObject ($this.ResultObject | convertto-json -Depth 99) } $primaryResult = $this.ResultObject | where-object TableKind -eq PrimaryResult if ($primaryResult) { write-host ($primaryResult.columns | out-string) write-host ($primaryResult.Rows | out-string) } if ($this.ResultObject.tables) { write-host "results: $($this.ResultObject.tables[0].rows.count) / $(((get-date) - $startTime).TotalSeconds) seconds to execute" -ForegroundColor DarkCyan if ($this.ResultObject.tables[0].rows.count -eq $this.limit) { write-warning "results count equals limit $($this.limit). results may be incomplete" } } else { write-warning "bad result: error:"#$($error)" } return $this.Pipe() } [KustoObj] ExecFunctionWithTableName([string]$function) { $functionObj = ($this.FunctionObjs.getEnumerator() | where-object Name -imatch $function).Value if (!$function -or !$functionObj -or $functionObj.parameters.length -lt 1) { write-warning "verify function '$function' and number of parameters '$($functionObj.parameters)'" } else { write-host "function:$function$($functionObj.parameters)" -foregroundcolor cyan } if ($this.Table) { $this.Exec([string]::Format("{0}('{1}')", $function, $this.Table)) } else { write-warning "table not set" } return $this.Pipe() } [KustoObj] ExecFunction([string]$function, [array]$parameters) { if ($parameters) { $this.Exec([string]::Format("{0}('{1}')", $function, $parameters -join "','")) } else { $this.Exec([string]::Format("{0}()", $function)) } return $this.Pipe() } [KustoObj] ExecScript([string]$script, [hashtable]$parameters) { $this.Script = $script $this.parameters = $parameters $this.ExecScript() $this.Script = $null return $this.Pipe() } [KustoObj] ExecScript([string]$script) { $this.Script = $script $this.ExecScript() $this.Script = $null return $this.Pipe() } [KustoObj] ExecScript() { if ($this.Script.startswith('http')) { $destFile = "$pwd\$([io.path]::GetFileName($this.Script))" -replace '\?.*', '' if (!(test-path $destFile)) { Write-host "downloading $($this.Script)" -foregroundcolor green invoke-webRequest $this.Script -outFile $destFile } else { Write-host "using cached script $($this.Script)" } $this.Script = $destFile } if ((test-path $this.Script)) { $this.Query = (Get-Content -raw -Path $this.Script) } else { write-error "unknown script:$($this.Script)" return $this.Pipe() } $this.Exec() return $this.Pipe() } [void] ExportCsv([string]$exportFile) { $this.CreateResultTable() [io.directory]::createDirectory([io.path]::getDirectoryName($exportFile)) $this.ResultTable | export-csv -notypeinformation $exportFile } [void] ExportJson([string]$exportFile) { $this.CreateResultTable() [io.directory]::createDirectory([io.path]::getDirectoryName($exportFile)) $this.ResultTable | convertto-json -depth 99 | out-file $exportFile } [string] FixColumns([string]$sourceContent) { if (!($this.fixDuplicateColumns)) { return $sourceContent } [hashtable]$tempTable = @{ } $matches = [regex]::Matches($sourceContent, '"ColumnName":"(?.+?)"', 1) foreach ($match in $matches) { $matchInfo = $match.Captures[0].Groups['columnName'] $column = $match.Captures[0].Groups['columnName'].Value $newColumn = $column $increment = $true $count = 0 while ($increment) { try { [void]$tempTable.Add($newColumn, $null) $increment = $false if ($newColumn -ne $column) { write-warning "replacing $column with $newColumn" return $this.FixColumns($sourceContent.Substring(0, $matchInfo.index) ` + $newColumn ` + $sourceContent.Substring($matchInfo.index + $matchinfo.Length)) } } catch { $count++ $newColumn = "$($column)_$($count)" $error.Clear() } } } return $sourceContent } [void] Import() { if ($this.Table) { $this.Import($this.Table) } else { write-warning "set table name first" return } } [KustoObj] Import([string]$table) { if (!$this.ResultObject.Tables) { write-warning 'no results to import' return $this.Pipe() } [object]$results = $this.ResultObject.Tables[0] [string]$formattedHeaders = "(" foreach ($column in ($results.Columns)) { $formattedHeaders += "['$($column.ColumnName)']:$($column.DataType.tolower()), " } $formattedHeaders = $formattedHeaders.trimend(', ') $formattedHeaders += ")" [text.StringBuilder]$csv = New-Object text.StringBuilder foreach ($row in ($results.rows)) { $csv.AppendLine($row -join ',') } $this.Exec(".drop table ['$table'] ifexists") $this.Exec(".create table ['$table'] $formattedHeaders") $this.Exec(".ingest inline into table ['$table'] <| $($csv.tostring())") return $this.Pipe() } [KustoObj] ImportCsv([string]$csvFile, [string]$table, [string]$headers) { $this.Headers = $headers $this.Table = $table $this.ImportCsv($csvFile) return $this.Pipe() } [KustoObj] ImportCsv([string]$csvFile, [string]$table) { $this.Table = $table $this.ImportCsv($csvFile) return $this.Pipe() } [KustoObj] ImportCsv([string]$csvFile) { if (!(test-path $csvFile) -or !$this.Table) { write-warning "verify importfile: $csvFile and import table: $($this.Table)" return $this.Pipe() } # not working #POST https://help.kusto.windows.net/v1/rest/ingest/Test/Logs?streamFormat=Csv HTTP/1.1 #[string]$csv = Get-Content -Raw $csvFile -encoding utf8 #$this.Post($csv) $sr = new-object io.streamreader($csvFile) [string]$tempHeaders = $sr.ReadLine() [text.StringBuilder]$csv = New-Object text.StringBuilder while ($sr.peek() -ge 0) { $csv.AppendLine($sr.ReadLine()) } $sr.close() $formattedHeaderList = @{ } [string]$formattedHeaders = "(" if(!$this.Headers) { foreach ($header in ($tempHeaders.Split(',').trim())) { $columnCount = 0 if (!$header) { $header = 'column' } [string]$normalizedHeader = $header.trim('`"').Replace(" ", "_") $normalizedHeader = [regex]::Replace($normalizedHeader, "\W", "") $uniqueHeader = $normalizedHeader while ($formattedHeaderList.ContainsKey($uniqueHeader)) { $uniqueHeader = $normalizedHeader + ++$columnCount } $formattedHeaderList.Add($uniqueHeader, "") $formattedHeaders += "['$($uniqueHeader)']:string, " } $this.Headers = $formattedHeaders } else { $formattedHeaders += $this.Headers } $formattedHeaders = $formattedHeaders.trimend(', ') $formattedHeaders += ")" #$this.Exec(".drop table ['$($this.Table)'] ifexists") $this.Exec(".create table ['$($this.Table)'] $formattedHeaders") $this.Exec(".ingest inline into table ['$($this.Table)'] <| $($csv.tostring())") return $this.Pipe() } [KustoObj] ImportJson([string]$jsonFile) { [string]$csvFile = [io.path]::GetTempFileName() try { ((Get-Content -Path $jsonFile) | ConvertFrom-Json) | Export-CSV $csvFile -NoTypeInformation write-host "using $csvFile" if (!(test-path $jsonFile) -or !$this.Table) { write-warning "verify importfile: $csvFile and import table: $($this.Table)" return $this.Pipe() } $this.ImportCsv($csvFile) return $this.Pipe() } finally { write-host "deleting $csvFile" [io.file]::Delete($csvFile) } } [KustoObj] ImportJson([string]$jsonFile, [string]$table) { $this.Table = $table $this.ImportJson($jsonFile) return $this.Pipe() } [bool] Logon([string]$resourceUrl) { [int]$expirationRefreshMinutes = 15 [int]$expirationMinutes = 0 if (!$resourceUrl) { write-warning "-resourceUrl required. example: https://{{ kusto cluster }}.kusto.windows.net" return $false } if ($this.authenticationResult) { $expirationMinutes = $this.authenticationResult.ExpiresOn.Subtract((get-date)).TotalMinutes } write-verbose "token expires in: $expirationMinutes minutes" if (!$this.Force -and ($expirationMinutes -gt $expirationRefreshMinutes)) { write-verbose "token valid: $($this.authenticationResult.ExpiresOn). use -force to force logon" return $true } #return $this.LogonMsal($resourceUrl, @("$resourceUrl/kusto.read", "$resourceUrl/kusto.write")) return $this.LogonMsal($resourceUrl, @("$resourceUrl/user_impersonation")) } hidden [bool] LogonMsal([string]$resourceUrl, [string[]]$scopes) { try { $error.Clear() [string[]]$defaultScope = @(".default") if ($this.clientId -and $this.clientSecret) { [string[]]$defaultScope = @("$resourceUrl/.default") [Microsoft.Identity.Client.ConfidentialClientApplicationOptions] $cAppOptions = new-Object Microsoft.Identity.Client.ConfidentialClientApplicationOptions $cAppOptions.ClientId = $this.clientId $cAppOptions.RedirectUri = $this.redirectUri $cAppOptions.ClientSecret = $this.clientSecret $cAppOptions.TenantId = $this.tenantId [Microsoft.Identity.Client.ConfidentialClientApplicationBuilder]$cAppBuilder = [Microsoft.Identity.Client.ConfidentialClientApplicationBuilder]::CreateWithApplicationOptions($cAppOptions) $cAppBuilder = $cAppBuilder.WithAuthority([microsoft.identity.client.azureCloudInstance]::AzurePublic, $this.tenantId) if ($global:PSVersionTable.PSEdition -eq "Core") { $cAppBuilder = $cAppBuilder.WithLogging($this.MsalLoggingCallback, [Microsoft.Identity.Client.LogLevel]::Verbose, $true, $true ) } $this.confidentialClientApplication = $cAppBuilder.Build() write-verbose ($this.confidentialClientApplication | convertto-json) try { write-host "acquire token for client" -foregroundcolor green $this.authenticationResult = $this.confidentialClientApplication.AcquireTokenForClient($defaultScope).ExecuteAsync().Result } catch [Exception] { write-host "error client acquire error: $_`r`n$($error | out-string)" -foregroundColor red $error.clear() } } else { # user creds [Microsoft.Identity.Client.PublicClientApplicationBuilder]$pAppBuilder = [Microsoft.Identity.Client.PublicClientApplicationBuilder]::Create($this.clientId) $pAppBuilder = $pAppBuilder.WithAuthority([microsoft.identity.client.azureCloudInstance]::AzurePublic, $this.tenantId) if (!($this.publicClientApplication)) { if ($global:PSVersionTable.PSEdition -eq "Core") { $pAppBuilder = $pAppBuilder.WithDefaultRedirectUri() $pAppBuilder = $pAppBuilder.WithLogging($this.MsalLoggingCallback, [Microsoft.Identity.Client.LogLevel]::Verbose, $true, $true ) } else { $pAppBuilder = $pAppBuilder.WithRedirectUri($this.redirectUri) } $this.publicClientApplication = $pAppBuilder.Build() } write-verbose ($this.publicClientApplication | convertto-json) [Microsoft.Identity.Client.IAccount]$account = $this.publicClientApplication.GetAccountsAsync().Result[0] #preauth with .default scope try { write-host "preauth acquire token silent for account: $account" -foregroundcolor green $this.authenticationResult = $this.publicClientApplication.AcquireTokenSilent($defaultScope, $account).ExecuteAsync().Result if (!$this.authenticationResult) { throw } } catch [Exception] { write-host "preauth acquire error: $_`r`n$($error | out-string)" -foregroundColor yellow $error.clear() try { write-host "preauth acquire token interactive" -foregroundcolor yellow $this.authenticationResult = $this.publicClientApplication.AcquireTokenInteractive($defaultScope).ExecuteAsync().Result if (!$this.authenticationResult) { throw } } catch [Exception] { write-host "preauth acquire token device" -foregroundcolor yellow $this.authenticationResult = $this.publicClientApplication.AcquireTokenWithDeviceCode($defaultScope, $this.MsalDeviceCodeCallback).ExecuteAsync().Result if (!$this.authenticationResult) { throw } } } write-host "authentication result: $($this.authenticationResult)" $account = $this.publicClientApplication.GetAccountsAsync().Result[0] #add kusto scopes after preauth if ($scopes) { try { write-host "kusto acquire token silent" -foregroundcolor green $this.authenticationResult = $this.publicClientApplication.AcquireTokenSilent($scopes, $account).ExecuteAsync().Result } catch [Exception] { write-host "kusto acquire error: $_`r`n$($error | out-string)" -foregroundColor red $error.clear() } } } if ($this.authenticationResult) { write-host "authenticationResult:$($this.authenticationResult | convertto-json)" $this.Token = $this.authenticationResult.AccessToken return $true } return $false } catch { Write-Error "$($error | out-string)" return $false } } [Threading.Tasks.Task] MsalDeviceCodeCallback([Microsoft.Identity.Client.DeviceCodeResult] $result) { write-host "MSAL Device code result: $($result | convertto-json)" return [threading.tasks.task]::FromResult(0) } [void] MsalLoggingCallback([Microsoft.Identity.Client.LogLevel] $level, [string]$message, [bool]$containsPII) { write-verbose "MSAL: $level $containsPII $message" } [KustoObj] Pipe() { if ($this.pipeLine) { return $this } return $null } hidden [object] Post([string]$body = "") { # authorize aad to get token [string]$kustoHost = "$($this.cluster).kusto.windows.net" [string]$kustoResource = "https://$kustoHost" [string]$csl = "$($this.Query)" $this.Result = $null $this.ResultObject = $null $this.ResultTable = $null $this.Query = $this.Query.trim() if ($body -and ($this.Table)) { $uri = "$kustoResource/v1/rest/ingest/$($this.Database)/$($this.Table)?streamFormat=Csv&mappingName=CsvMapping" } elseif ($this.Query.startswith('.show') -or !$this.Query.startswith('.')) { $uri = "$kustoResource/v1/rest/query" $csl = "$($this.Query) | limit $($this.Limit)" } else { $uri = "$kustoResource/v1/rest/mgmt" } if (!$this.Token -or $this.authenticationResult) { if (!($this.Logon($kustoResource))) { write-error "unable to acquire token." return $error } } $requestId = [guid]::NewGuid().ToString() write-verbose "request id: $requestId" $header = @{ 'accept' = 'application/json' 'authorization' = "Bearer $($this.Token)" 'content-type' = 'application/json' 'host' = $kustoHost 'x-ms-app' = 'kusto-rest.ps1' 'x-ms-user' = $env:USERNAME 'x-ms-client-request-id' = $requestId } if ($body) { $header.Add("content-length", $body.Length) } else { $body = @{ db = $this.database csl = $csl properties = @{ Options = @{ queryconsistency = "strongconsistency" servertimeout = $this.ServerTimeout.ToString() } Parameters = $this.parameters } } | ConvertTo-Json } write-verbose ($header | convertto-json) write-verbose $body $error.clear() $this.Result = Invoke-WebRequest -Method Post -Uri $uri -Headers $header -Body $body write-verbose $this.Result if ($error) { return $error } try { return ($this.FixColumns($this.Result.content) | convertfrom-json) } catch { write-warning "error converting json result to object. unparsed results in `$this.Result`r`n$error" if (!$this.FixDuplicateColumns) { write-warning "$this.fixDuplicateColumns = $true may resolve." } return ($this.Result.content) } } [collections.arrayList] RemoveEmptyResults([collections.arrayList]$sourceContent) { if (!$this.RemoveEmptyColumns -or !$sourceContent -or $sourceContent.count -eq 0) { return $sourceContent } $columnList = (Get-Member -InputObject $sourceContent[0] -View Extended).Name write-verbose "checking column list $columnList" $populatedColumnList = [collections.arraylist]@() foreach ($column in $columnList) { if (@($sourceContent | where-object $column -ne "").Count -gt 0) { $populatedColumnList += $column } } return [collections.arrayList]@($sourceContent | select-object $populatedColumnList) } [KustoObj] SetCluster([string]$cluster) { $this.Cluster = $cluster return $this.Pipe() } [KustoObj] SetDatabase([string]$database) { $this.Database = $database $this.SetTables() $this.SetFunctions() return $this.Pipe() } [KustoObj] SetPipe([bool]$enable) { $this.PipeLine = $enable return $this.Pipe() } [KustoObj] SetTable([string]$table) { $this.Table = $table return $this.Pipe() } [KustoObj] SetFunctions() { $this.Functions.Clear() $this.FunctionObjs.Clear() $this.exec('.show functions') $this.CreateResultTable() foreach ($function in $this.ResultTable) { $this.Functions.Add($function.Name, $function.Name) $this.FunctionObjs.Add($function.Name, $function) } return $this.Pipe() } [KustoObj] SetTables() { $this.Tables.Clear() $this.exec('.show tables | project TableName') $this.CreateResultTable() foreach ($table in $this.ResultTable) { $this.Tables.Add($table.TableName, $table.TableName) } return $this.Pipe() } } # comment next line after microsoft.identity.client type has been imported into powershell session to troubleshoot 2 of 2 '@ $error.Clear() $global:kusto = [KustoObj]::new() $kusto.SetTables() $kusto.SetFunctions() $kusto.Exec() $kusto.ClearResults() write-host ($PSBoundParameters | out-string) if ($error) { write-warning ($error | out-string) } else { write-host ($kusto | Get-Member | out-string) write-host "use `$kusto object to set properties and run queries. example: `$kusto.Exec('.show operations')" -ForegroundColor Green write-host "set `$kusto.viewresults=`$true to see results." -ForegroundColor Green }