When passing in objects to Get-SkylineAffectedObject, it was possible for duplicate queries to be invoked for the same product. This commit fixes that by moving some of the string manipulation inside the correct loop for this function. Additionally, the text replace lines were consolidated to reduce some confusion with variable assignment that led to this bug in the first place. Signed-off-by: Brian Wuchner <brian.wuchner@gmail.com>
423 lines
18 KiB
PowerShell
423 lines
18 KiB
PowerShell
<#
|
|
Copyright 2021 VMware, Inc.
|
|
SPDX-License-Identifier: BSD-2-Clause
|
|
#>
|
|
|
|
Function Connect-SkylineInsights {
|
|
<#
|
|
.NOTES
|
|
===========================================================================
|
|
Created by: Brian Wuchner
|
|
Date: February 21, 2022
|
|
Blog: www.enterpriseadmins.org
|
|
Twitter: @bwuch
|
|
===========================================================================
|
|
.SYNOPSIS
|
|
Use this function to create the auth header to connect to Skyline Insights API
|
|
.DESCRIPTION
|
|
This function will allow you to connect to a Skyline Insights API.
|
|
A global variable will be set with the Servername & Header value for use by other functions.
|
|
.EXAMPLE
|
|
PS C:\> Connect-SkylineInsights -apiKey 'my-key-from-csp'
|
|
This will use the provided API key to create a connection to Skyline Insights.
|
|
.EXAMPLE
|
|
PS C:\> Connect-SkylineInsights -apiKey 'my-key-from-csp' -SaveCredentials
|
|
This will use the PowerCLI VICredentialStore Item to save the provided API key. On next use this key will be provided automatically.
|
|
#>
|
|
param(
|
|
[string]$apiKey,
|
|
[switch]$SaveCredentials,
|
|
[Parameter(DontShow)]$cspApi = 'console.cloud.vmware.com',
|
|
[Parameter(DontShow)]$skylineApi = 'skyline.vmware.com'
|
|
)
|
|
|
|
if ($PSEdition -eq 'Core' -And $SaveCredentials) {
|
|
write-error 'The parameter SaveCredentials of Connect-SkylineInsights cmdlet is not supported on PowerShell Core.'
|
|
return
|
|
}
|
|
|
|
if ($PSEdition -eq 'Core' -AND !$apiKey) {
|
|
write-error 'An API key is required.'
|
|
return
|
|
}
|
|
|
|
# Create VICredentialStore item to save the API key
|
|
if ($apiKey -AND $SaveCredentials) {
|
|
if ( (Get-Command Get-VICredentialStoreItem -ErrorAction:SilentlyContinue | Measure-Object).Count -gt 0 ) {
|
|
$savedCred = Get-VICredentialStoreItem -host $skylineApi -ErrorAction:SilentlyContinue
|
|
if ($savedCred) {
|
|
$savedCred | Remove-VICredentialStoreItem -Confirm:$false
|
|
}
|
|
New-VICredentialStoreItem -Host $skylineApi -User 'api-key' -Password $apiKey
|
|
} else {
|
|
Write-Warning 'Use of -SaveCredentials requires the PowerCLI VICredentialStoreItem cmdlets.'
|
|
}
|
|
}
|
|
|
|
if (!$apiKey) {
|
|
if ( (Get-Command Get-VICredentialStoreItem -ErrorAction:SilentlyContinue | Measure-Object).Count -gt 0 ) {
|
|
$savedCred = Get-VICredentialStoreItem -host $skylineApi -ErrorAction:SilentlyContinue
|
|
}
|
|
if ( ($savedCred | Measure-Object).Count -eq 1) {
|
|
$apiKey = $savedCred.Password
|
|
} else {
|
|
write-error 'An API key is required.'
|
|
return
|
|
}
|
|
}
|
|
|
|
$loginHeader = @{
|
|
'Accept' = 'application/json'
|
|
'Content-Type' = 'application/x-www-form-urlencoded'
|
|
}
|
|
$loginBody = @{'refresh_token' = $apiKey }
|
|
|
|
try {
|
|
$webRequest = Invoke-RestMethod -Uri "https://$cspApi/csp/gateway/am/api/auth/api-tokens/authorize?grant_type=refresh_token" -method POST -Headers $loginHeader -Body $loginBody
|
|
|
|
$global:DefaultSkylineConnection = New-Object psobject -property @{ 'Name'=$skylineApi; 'CSPName'=$cspApi; 'ConnectionDetail'=$webRequest; APIKey = $apiKey;
|
|
'Refresh_Token'=$webRequest.refresh_token; 'SkylineAPI'="https://$skylineApi/public/api/data"; PSTypeName='SkylineConnection' }
|
|
|
|
# Return the connection object
|
|
$global:SkylineInsightsApiQueryCount = 0
|
|
$global:SkylineInsightsApiQueryLastTime = $null
|
|
$global:DefaultSkylineConnection
|
|
} catch {
|
|
Write-Error ("Failure connecting to $skylineAPI. Posted $loginBody " + $_)
|
|
} # end try/catch block
|
|
}
|
|
|
|
Function Disconnect-SkylineInsights {
|
|
<#
|
|
.NOTES
|
|
===========================================================================
|
|
Created by: Brian Wuchner
|
|
Date: February 21, 2022
|
|
Blog: www.enterpriseadmins.org
|
|
Twitter: @bwuch
|
|
===========================================================================
|
|
.SYNOPSIS
|
|
Use this function to disconnect from Skyline Insights API
|
|
.DESCRIPTION
|
|
This function will allow you to disconnect from a Skyline Insights API.
|
|
The global variable will be set with the Servername & Header value for use by other functions.
|
|
.EXAMPLE
|
|
PS C:\> Disconnect-SkylineInsights
|
|
This will remove a connection to Skyline Insights.
|
|
#>
|
|
if ($global:DefaultSkylineConnection) {
|
|
$global:DefaultSkylineConnection = $null
|
|
} else {
|
|
Write-Error 'Could not find an existing connection to SkylineInsights API.'
|
|
}
|
|
}
|
|
|
|
Function Invoke-SkylineInsightsApi {
|
|
<#
|
|
.NOTES
|
|
===========================================================================
|
|
Created by: Brian Wuchner
|
|
Date: February 21, 2022
|
|
Blog: www.enterpriseadmins.org
|
|
Twitter: @bwuch
|
|
===========================================================================
|
|
.SYNOPSIS
|
|
Use this function to post a query to the Skyline Insights API.
|
|
.DESCRIPTION
|
|
This function will allow you to query the Skyline Insights API.
|
|
Proper headers will be formatted and posted if a DefaultSkylineConnection is present.
|
|
This is primarily a helper function used by other functions included in the module.
|
|
It is exported in the module manifest to be used for any custom queries.
|
|
.EXAMPLE
|
|
PS C:\> Invoke-SkylineInsightsApi -queryBody '{formatted-query-string-converted-to-json}'
|
|
#>
|
|
param(
|
|
[Parameter(Mandatory=$true)][string]$queryBody,
|
|
[Parameter(DontShow=$true)][int]$sleepTimerMs=501
|
|
)
|
|
|
|
if ( !$global:DefaultSkylineConnection ) {
|
|
Write-Error 'You are not currently connected to any servers. Please connect first using Connect-SkylineInsights.'
|
|
return;
|
|
}
|
|
|
|
write-debug "Querybody: $queryBody"
|
|
try {
|
|
if ($global:SkylineInsightsApiQueryLastTime) {
|
|
$timeSinceLastQuery = (New-TimeSpan $global:SkylineInsightsApiQueryLastTime (Get-Date)).TotalMilliseconds
|
|
if ($timeSinceLastQuery -lt $sleepTimerMs) {
|
|
Write-Debug "Waiting $($sleepTimerMs-$timeSinceLastQuery)ms to prevent HTTP 429 TOO_MANY_REQUESTS error"
|
|
Start-Sleep -Milliseconds ($sleepTimerMs-$timeSinceLastQuery)
|
|
}
|
|
}
|
|
$restCall = invoke-restmethod -method post -Uri $($global:DefaultSkylineConnection.SkylineAPI) -Headers @{Authorization = "Bearer $($global:DefaultSkylineConnection.ConnectionDetail.access_token)"} -body $queryBody -ContentType "application/json"
|
|
$global:SkylineInsightsApiQueryCount++
|
|
$global:SkylineInsightsApiQueryLastTime = Get-Date
|
|
if ($restCall.errors) {
|
|
Write-Error $restCall.errors.Message
|
|
}
|
|
return $restCall
|
|
} catch {
|
|
$incomingError = $_
|
|
try {
|
|
# are nested try/catch blocks the powershell equilivent of vbscript On Error Resume Next?
|
|
$errorStatusAsJson = ($incomingError | ConvertFrom-Json).status
|
|
if ($errorStatusAsJson -eq '429 TOO_MANY_REQUESTS') {
|
|
write-error 'Encountered HTTP 429 TOO_MANY_REQUESTS error, consider increasing sleepTimerMs value.'
|
|
start-sleep -Milliseconds (2*$sleepTimerMs)
|
|
break
|
|
}
|
|
} catch {
|
|
# this was the error from trying to cast the incoming error to Json
|
|
}
|
|
if (!$errorStatusAsJson) { write-error $incomingError }
|
|
}
|
|
}
|
|
|
|
|
|
Function Get-SkylineFinding {
|
|
<#
|
|
.NOTES
|
|
===========================================================================
|
|
Created by: Brian Wuchner
|
|
Date: February 21, 2022
|
|
Blog: www.enterpriseadmins.org
|
|
Twitter: @bwuch
|
|
===========================================================================
|
|
.SYNOPSIS
|
|
Use this function to query findings from the Skyline Insights API.
|
|
.DESCRIPTION
|
|
This function will allow you to query the Skyline Insights API for Findings.
|
|
As described in the documentation, the maximum limit per page is 200 records. This function provides
|
|
an optional pagesize parameter to request smaller batches, but by default assumes 200 records.
|
|
.EXAMPLE
|
|
PS C:\> Get-SkylineFinding
|
|
#>
|
|
[cmdletbinding()]
|
|
param(
|
|
[Parameter(ValueFromPipelineByPropertyName=$true)][string]$findingId,
|
|
[Parameter(ValueFromPipelineByPropertyName=$true)][string[]]$products,
|
|
[Parameter(ValueFromPipelineByPropertyName=$true)][ValidateSet('CRITICAL','MODERATE','TRIVIAL')][string]$severity,
|
|
[Parameter(DontShow=$true)][ValidateRange(1,200)][int]$pagesize=200
|
|
)
|
|
|
|
begin {
|
|
$queryBody = @"
|
|
{
|
|
activeFindings(limit: $pagesize, start: 0 filter: {}) {
|
|
findings {
|
|
findingId
|
|
accountId
|
|
findingDisplayName
|
|
severity
|
|
products
|
|
findingDescription
|
|
findingImpact
|
|
recommendations
|
|
kbLinkURLs
|
|
recommendationsVCF
|
|
kbLinkURLsVCF
|
|
categoryName
|
|
findingTypes
|
|
firstObserved
|
|
totalAffectedObjectsCount
|
|
}
|
|
totalRecords
|
|
timeTaken
|
|
}
|
|
}
|
|
"@
|
|
|
|
}
|
|
process {
|
|
if (!$products) { $products = 'NO_PRODUCT_FILTER'}
|
|
foreach ($thisProduct in $products) {
|
|
if ($findingId) { $filterString = "findingId: `"$findingId`"," }
|
|
if ($thisProduct -ne 'NO_PRODUCT_FILTER') { $filterString += "product: `"$thisProduct`"," }
|
|
|
|
# Try to get results the first time
|
|
$results = @()
|
|
$thisIteration = 0
|
|
do {
|
|
$thisQueryBody = $queryBody -Replace 'filter: {}', "filter: { $filterString }" -Replace 'start: 0', "start: $thisIteration"
|
|
Write-Debug $thisQueryBody
|
|
$thisResult = Invoke-SkylineInsightsApi -queryBody (@{'query' = $thisQueryBody} | ConvertTo-Json -Compress)
|
|
$totalRecords = $thisResult.data.activeFindings.totalRecords
|
|
if ($severity) {
|
|
$thisResult.data.activeFindings.Findings | Where-Object {$_.severity -eq $severity}
|
|
} else {
|
|
$thisResult.data.activeFindings.Findings
|
|
}
|
|
$results += ($thisResult.data.activeFindings.Findings)
|
|
$thisIteration += $pageSize
|
|
} while ($results.count -lt $totalRecords ) # end do/while loop
|
|
|
|
#return $results
|
|
}
|
|
}
|
|
end {
|
|
|
|
}
|
|
}
|
|
|
|
Function Get-SkylineAffectedObject {
|
|
<#
|
|
.NOTES
|
|
===========================================================================
|
|
Created by: Brian Wuchner
|
|
Date: February 21, 2022
|
|
Blog: www.enterpriseadmins.org
|
|
Twitter: @bwuch
|
|
===========================================================================
|
|
.SYNOPSIS
|
|
Use this function to query affected objects from the Skyline Insights API.
|
|
.DESCRIPTION
|
|
This function will allow you to query the Skyline Insights API for affected objects.
|
|
Input parameters are required for the findingId and product. Products can be provided as an object (from Get-SkylineFinding) or
|
|
a single product can be specified by name (or delimited list).
|
|
As described in the documentation, the maximum limit per page is 200 records. This function provides
|
|
an optional pagesize parameter to request smaller batches, but by default assumes 200 records.
|
|
.EXAMPLE
|
|
PS C:\> Get-SkylineAffectedObject -findingId 'vSphere-Vmtoolsmemoryleak-KB#76163' -product 'core-vcenter01.lab.enterpriseadmins.org'
|
|
This example uses the ByName parameter set to pass in specific findings/product and expects either a single product or a 'separator' delimited list
|
|
.EXAMPLE
|
|
PS C:\> Get-SkylineFinding | Select-Object -First 2 | Get-SkylineAffectedObject
|
|
This example uses the ByObject parameter set to pass in products as an object from Get-SkylineFinding
|
|
#>
|
|
[cmdletbinding()]
|
|
param(
|
|
[Parameter(Mandatory=$true, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)][string]$findingId,
|
|
[Parameter(Mandatory=$true, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)][string[]]$products,
|
|
[Parameter(DontShow=$true)][ValidateRange(1,200)][int]$pagesize=200
|
|
)
|
|
|
|
begin {
|
|
$queryBody = @"
|
|
{
|
|
activeFindings(
|
|
filter: {
|
|
findingId: "",
|
|
product: "",
|
|
}) {
|
|
findings {
|
|
totalAffectedObjectsCount
|
|
affectedObjects(start: 0, limit: $pagesize) {
|
|
sourceName
|
|
objectName
|
|
objectType
|
|
version
|
|
buildNumber
|
|
solutionTags {
|
|
type
|
|
version
|
|
}
|
|
firstObserved
|
|
}
|
|
}
|
|
totalRecords
|
|
timeTaken
|
|
}
|
|
}
|
|
"@
|
|
|
|
# Try to get results the first time
|
|
}
|
|
|
|
process {
|
|
foreach ( $thisProduct in $products ) {
|
|
$thisIteration = 0
|
|
$results = @() # reset results variable between products
|
|
do {
|
|
$thisQueryBody = $queryBody -Replace 'product: "",', "product: `"$thisProduct`"," -Replace 'start: 0', "start: $thisIteration" -Replace 'findingId: "",', "findingId: `"$findingId`","
|
|
Write-Debug $thisQueryBody
|
|
$thisResult = Invoke-SkylineInsightsApi -queryBody (@{'query' = $thisQueryBody} | ConvertTo-Json -Compress)
|
|
$totalRecords = $thisResult.data.activeFindings.Findings.totalAffectedObjectsCount
|
|
$thisResult.data.activeFindings.Findings.affectedObjects | Select-Object @{N='findingId';E={$findingId}}, *
|
|
$results += ($thisResult.data.activeFindings.Findings.affectedObjects) | Select-Object @{N='findingId';E={$findingId}}, *
|
|
$thisIteration += $pagesize
|
|
} while ($results.count -lt $totalRecords ) # end do/while loop
|
|
} # end foreach product loop
|
|
}
|
|
}
|
|
|
|
Function Format-SkylineResult {
|
|
<#
|
|
.NOTES
|
|
===========================================================================
|
|
Created by: Brian Wuchner
|
|
Date: February 21, 2022
|
|
Blog: www.enterpriseadmins.org
|
|
Twitter: @bwuch
|
|
===========================================================================
|
|
.SYNOPSIS
|
|
Use this function to format results from the Skyline Insights API
|
|
.DESCRIPTION
|
|
This function will format the output from the Skyline Insights API.
|
|
For example, Get-SkylineFinding and Get-SkylineAffectedObject will return some strings, date values as numbers, and object properties.
|
|
This function will convert date numbers to powershell dates and objects to delimiter separated stings. This should help with exporting
|
|
results to CSV files for example.
|
|
.EXAMPLE
|
|
PS C:\> Get-SkylineFinding | Format-SkylineResult | Export-Csv c:\temp\findings.csv -NoTypeInformation
|
|
This will return Skyline Findings, format them as needed, and export results to a CSV file.
|
|
#>
|
|
param(
|
|
[Parameter(Mandatory=$true, ValueFromPipeline=$true)][PSCustomObject]$inputObject,
|
|
[string]$separator = '; '
|
|
)
|
|
begin {
|
|
$results = @()
|
|
|
|
# To format the dates, we need to add the value returned by the API to the begining of time
|
|
$startOfTime = Get-Date '1970-01-01'
|
|
}
|
|
|
|
process {
|
|
if ( $inputObject.accountId ) {
|
|
#This appears to be a Finding
|
|
$results += $inputObject | Select-Object findingId, accountId, findingDisplayName, severity, @{N='product';E={[string]::join($separator, $_.products)}}, findingDescription,
|
|
findingImpact, @{N='recommendations';E={[string]::Join($separator,$_.recommendations)}}, @{N='kbLinkURLs';E={[string]::Join($separator, $_.kbLinkURLs)}},
|
|
@{N='recommendationsVCF';E={[string]::Join($separator,$_.recommendationsVCF)}}, @{N='kbLinkURLsVCF';E={[string]::Join($separator, $_.kbLinkURLsVCF)}},
|
|
categoryName, @{N='findingTypes';E={[string]::Join($sep, $_.findingTypes)}}, @{N='firstObserved';E={ $startOfTime+[timespan]::FromMilliseconds($_.firstObserved) }},
|
|
totalAffectedObjectsCount
|
|
|
|
} elseif ( $inputObject.objectName ) {
|
|
#This appears to be an AffectedObject
|
|
$results += $inputObject | Select-Object findingId, sourceName, objectName, objectType, version, buildNumber, @{N='solutionTags-Type';E={$_.solutionTags.type}},
|
|
@{N='solutionTags-Version';E={$_.solutionTags.version}}, @{N='firstObserved';E={ $startOfTime+[timespan]::FromMilliseconds($_.firstObserved) }}
|
|
} else {
|
|
write-warning "Unable to determine input object type."
|
|
} # end inputobject evaluation
|
|
} #end process
|
|
|
|
end {
|
|
return $results
|
|
}
|
|
}
|
|
|
|
Function Start-SkylineInsightsApiExplorer {
|
|
<#
|
|
.NOTES
|
|
===========================================================================
|
|
Created by: Brian Wuchner
|
|
Date: February 21, 2022
|
|
Blog: www.enterpriseadmins.org
|
|
Twitter: @bwuch
|
|
===========================================================================
|
|
.SYNOPSIS
|
|
Use this function to launch the Skyline Insights API in a browser.
|
|
.DESCRIPTION
|
|
This function will open the Skyline Insights API explorer in the default web browser and populate
|
|
the clipboard with the necessary authorization header value to enable interactive queries.
|
|
.EXAMPLE
|
|
PS C:\> Start-SkylineInsightsApiExplorer
|
|
#>
|
|
if ( !$global:DefaultSkylineConnection ) {
|
|
Write-Error 'You are not currently connected to any servers. Please connect first using Connect-SkylineInsights.'
|
|
return;
|
|
}
|
|
"Default web browser will launch to the Skyline Insights API explorer. In the lower left select 'Request Headers' and paste the authorization/bearer token into the text box. `nNote: this script has updated your clipboard with the required auth token."
|
|
"{`"Authorization`":`"Bearer $($global:DefaultSkylineConnection.ConnectionDetail.access_token)`"}" | Set-Clipboard
|
|
Start-Process "https://$($global:DefaultSkylineConnection.Name)/public/api/docs"
|
|
}
|
|
|