Private/Operations.ps1

<#
.SYNOPSIS
Invokes a Kusto query.
 
.DESCRIPTION
Invokes a Kusto query by calling the Kusto SDK that is loaded when the module is imported.
Builds a Kusto connection string and executes a query against a database in a cluster.
Connections utilize AAD credentials to avoid getting into credential management.
Clusters must exist in the *.kusto.windows.net subdomain.
Queries have a 45 second threshold before timing out.
 
.EXAMPLE
$query = "..."
Invoke-Kusto -Cluster 'AzureCM' -Database 'AzureCM' -Query $query
 
.NOTES
Initial framework author: Cale Vernon (CAVERNON)
#>


function Invoke-Kusto {
    [CmdletBinding()]
    param (
        [Parameter(
            Mandatory = $TRUE
        )][string]$kustoCluster,
        [Parameter(
            Mandatory = $TRUE
        )][string]$kustoDatabase,
        [Parameter(
            Mandatory = $TRUE
        )][string]$query
    )

    Try {
        # Create a Kusto connection string.
        $connectionStringBuilder = New-Object Kusto.Data.KustoConnectionStringBuilder("https://$kustoCluster.kusto.windows.net;AAD Federated Security=True", $kustoDatabase)
        $queryProvider = [Kusto.Data.Net.Client.KustoClientFactory]::CreateCslQueryProvider($connectionStringBuilder)
        $clientRequestProperties = New-Object Kusto.Data.Common.ClientRequestProperties

        # Set $clientRequestProperties, specifically a GUID for identification and a timeout threshold of one minute.
        $clientRequestProperties.ClientRequestId = 'MyPowerShellScript.ExecuteQuery.' + [guid]::NewGuid().Tostring()
        $clientRequestProperties.SetOption([Kusto.Data.Common.ClientRequestProperties]::OptionServerTimeout, [TimeSpan]::FromSeconds(45))

        # Execute the query and retrieve the resulting data table as a DataView.
        $reader = $queryProvider.ExecuteQuery($query, $clientRequestProperties)
        $dataTable = [Kusto.Cloud.Platform.Data.ExtendedDataReader]::ToDataSet($reader).Tables[0]

        # Create and return a $results object.
        $results = New-Object System.Data.DataView($dataTable)
        return $results
    }
    Catch {
        Write-Console -Message 'Kusto returned a critical error, such as a timeout or temporary throttle.' -Color 'Red'
        Write-Console -Message 'Try again momentarily as this type of error is typically transient.' -Color 'Red'
        Return
    }
}

function Invoke-NoInstancesFound {
    Write-Console -Message "No instances of $($Global:session.resource) were found in the given timespan."
    $timestamp = Get-Date -Format FileDateTimeUniversal
    $filename = "instances-$timestamp"
    $query | Out-String | Out-File -FilePath "$($Global:configuration.Output)\Temporary\$filename.csl" -Force
    Write-Console -Message 'Opening the query in Kusto Explorer for you to review.'
    Invoke-Item "$($Global:configuration.Output)\Temporary\$filename.csl"
}

function Invoke-Operation {
    [CmdletBinding()]
    param (
        [Parameter(
            Mandatory = $TRUE
        )][PSCustomObject]$operation
    )

    # Open any reference links.
    If ($operation.links) {
        Write-Console -Message "Opening the reference links for $($operation.name)..."
        ForEach ($link in $operation.links) {
            Start-Process $link
        }
    }

    # Ask for any parameters not already stored in $Global:session.
    ForEach ($parameter in $operation.parameters) {
        If (!($Global:session.$parameter)) {
            Write-Console -Message $parameter -Color 'Cyan'
            $parameterValue = Read-Host '?'
            If (!($parameterValue)) {
                Do {
                    $parameterValue = Read-Host '?'
                }
                Until ($parameterValue)
            }
            $Global:session | Add-Member -NotePropertyName $parameter -NotePropertyValue $parameterValue
        }   
    }

    # Log the operation use.
    Add-Log -Operation $operation.id

    # Validate the operation type.
    Switch ($operation.type) {
        'Global' {
            ForEach ($dashboard in $operation.body) {
                ForEach ($parameter in $operation.parameters) {
                    $matchingString = '`$' + $parameter
                    If ($parameter -eq 'timespanStart') {
                        $dashboard = ($dashboard).Replace($matchingString, $Global:session.timespanStartGlobal)
                    }
                    ElseIf ($parameter -eq 'timespanEnd') {
                        $dashboard = ($dashboard).Replace($matchingString, $Global:session.timespanEndGlobal)
                    }
                    Else {
                        $dashboard = ($dashboard).Replace($matchingString, $($Global:session.$parameter))
                    }
                }
                Write-Console -Message "Launching the following dashboard: $($operation.name)..."
                Start-Process $dashboard
            }
            # Break the Switch statement.
            Break
        }
        'Kusto' {
            $connectionTokens = ($operation.body[0]).Split('.')
            $kustoCluster = ($connectionTokens[0].Split("'"))[1] 
            $kustoDatabase = ($connectionTokens[1].Split("'"))[1]
            $query = $operation.body -Join "`r`n"
            ForEach ($parameter in $operation.parameters) {
                $matchingString = '`$' + $parameter
                If ($parameter -eq 'timespanStart') {
                    # Calculate the Kusto timestamp format if it does not already exist, such when using Free Mode.
                    If (!($Global:session.timespanStartStandard)) {
                        $timespanStartStandard = "$(Get-Date $Global:session.timespanStart -Format s)Z"
                        $Global:session | Add-Member -NotePropertyName 'timespanStartStandard' -NotePropertyValue $timespanStartStandard
                    }
                    $query = ($query).Replace($matchingString, $Global:session.timespanStartStandard)
                }
                ElseIf ($parameter -eq 'timespanEnd') {
                    # Calculate the Kusto timestamp format if it does not already exist, such when using Free Mode.
                    If (!($Global:session.timespanEndStandard)) {
                        $timespanEndStandard = "$(Get-Date $Global:session.timespanEnd -Format s)Z"
                        $Global:session | Add-Member -NotePropertyName 'timespanEndStandard' -NotePropertyValue $timespanEndStandard
                    }
                    $query = ($query).Replace($matchingString, $Global:session.timespanEndStandard)
                }
                Else {
                    $query = ($query).Replace($matchingString, $($Global:session.$parameter))
                } 
            }
            # Begin Kusto invocation.
            $results = Invoke-Kusto -KustoCluster $kustoCluster -KustoDatabase $kustoDatabase -Query $query
            
            # Begin results handling.
            # No results were found.
            If ($results.Count -eq 0) {
                Write-Console -Message "$($operation.name): No results were returned."
                $timestamp = Get-Date -Format FileDateTimeUniversal
                $filename = "$($operation.name)-$timestamp"
                $query | Out-String | Out-File -FilePath "$($Global:configuration.Output)\Temporary\$filename.csl" -Force
                Write-Console -Message "Opening the $($operation.name) query in Kusto Explorer for you to review."
                Invoke-Item "$($Global:configuration.Output)\Temporary\$filename.csl"
            }
            # Results were found.
            Else {
                Write-Console -Message "$($operation.name): $($results.Count) result(s) returned."
                $records = $results | Out-GridView -PassThru -Title "[Nanite] $($operation.name): Please select from the $($results.Count) record(s) returned."
                # No records were selected..
                If (!($records)) {
                    Write-Console -Message 'No records were selected.'
                    Return
                }
                # Records were selected.
                Else {
                    # # Entering a continuous loop to ensure the results handling menu is shown until exited.
                    While ($TRUE) {
                        # Show the results actions menu.
                        Do
                        {
                            Write-Console -Message "What would you like to do with the selected record(s)?"
                            Write-Console -Message "[B] Copy both the $($operation.name) query and records list to clipboard" -Color 'Cyan'
                            Write-Console -Message '[L] Copy records list to clipboard' -Color 'Cyan'
                            Write-Console -Message "[K] Open the $($operation.name) query in Kusto Explorer" -Color 'Cyan'
                            Write-Console -Message "[Q] Copy the $($operation.name) query to your clipboard" -Color 'Cyan'
                            Write-Console -Message '[S] Save the records as a comma-separated values (*.csv) file' -Color 'Cyan'
                            Write-Console -Message '[X] Run another operation.' -Color 'Cyan'
                            $selection = Read-Host '?'
                        }
                        Until (@('B', 'L', 'K', 'Q', 'S', 'X') -contains $selection)
                        Switch ($selection)
                        {
                            'B' {
                                $query | Out-String | Set-Clipboard
                                $records | Format-List | Out-String | Set-Clipboard -Append
                                Write-Console -Message "Copied both the $($operation.name) query and the record(s) list to clipboard."
                                Break
                            }
                            'L' {
                                $records | Format-List | Out-String | Set-Clipboard
                                Write-Console -Message "Copied the record(s) list to clipboard."
                                Break
                            }
                            'K' {
                                $timestamp = Get-Date -Format FileDateTimeUniversal
                                $filename = "$($operation.name)-$timestamp"
                                $query | Out-String | Out-File -FilePath "$($Global:configuration.Output)\Temporary\$filename.csl" -Force
                                Write-Console -Message 'Created temporary query file; opening.'
                                Invoke-Item "$($Global:configuration.Output)\Temporary\$filename.csl"
                                Break
                            }
                            'Q' {
                                $query | Out-String | Set-Clipboard
                                Write-Console -Message "Copied the $($operation.name) query to your clipboard."
                                Break
                            }
                            'S' {
                                $timestamp = Get-Date -Format FileDateTimeUniversal
                                $filename = "$($operation.name)-$timestamp"
                                $records | Export-Csv -Path "$($Global:configuration.Output)\Temporary\$filename.csv" -Delimiter ',' -NoTypeInformation -Force
                                Write-Console -Message "Saved the record(s) to $($Global:configuration.Output)\Temporary\$filename.csv."
                                Invoke-Item "$($Global:configuration.Output)\Temporary\$filename.csv"
                                Break
                            }
                            'X' {
                                Write-Console 'Returning to the operations menu...'
                                Return
                                Break
                            }
                        }
                    }
                }
            }
        }
        'Standard' {
            ForEach ($dashboard in $operation.body) {
                ForEach ($parameter in $operation.parameters) {
                    $matchingString = '`$' + $parameter
                    If ($parameter -eq 'timespanStart') {
                        $dashboard = ($dashboard).Replace($matchingString, $Global:session.timespanStartStandard)
                    }
                    ElseIf ($parameter -eq 'timespanEnd') {
                        $dashboard = ($dashboard).Replace($matchingString, $Global:session.timespanEndStandard)
                    }
                    Else {
                        $dashboard = ($dashboard).Replace($matchingString, $($Global:session.$parameter))
                    }
                }
                Write-Console -Message "Launching the following dashboard: $($operation.name)..."
                Start-Process $dashboard
            }
            # Break the Switch statement.
            Break
        }
        Default {
            Write-Console -Message "Operation $($operation.name) is an unsupported type: $($operation.type)"
            Break
        }
    }
}