Public/Queries/Invoke-FleetQuery.ps1
function Invoke-FleetQuery { <# .SYNOPSIS Executes a live query on FleetDM hosts .DESCRIPTION Runs an osquery SQL statement on specified hosts and returns the results. For ad-hoc queries, automatically creates a temporary saved query, runs it, retrieves the results, and cleans up. This provides actual query results instead of just campaign information. .PARAMETER Query The SQL query to execute. This should be a valid osquery SQL statement. .PARAMETER QueryId The ID of a saved query to execute (alternative to providing Query text) .PARAMETER HostId Array of host IDs to run the query on. Accepts pipeline input. .PARAMETER Label Array of label names to run the query on all hosts with those labels .PARAMETER All Run the query on all hosts in the fleet (not supported by FleetDM API for ad-hoc queries) .PARAMETER Wait For saved queries (QueryId), returns results directly instead of starting a campaign. This parameter is deprecated for ad-hoc queries as they now always return results. .PARAMETER MaxWaitTime Maximum time to wait for results in seconds (default: 25) .EXAMPLE $results = Invoke-FleetQuery -Query "SELECT * FROM system_info;" -HostId 1,2,3 $results.Results | Format-Table Runs a system info query on specific hosts and returns the actual results .EXAMPLE $hosts = Get-FleetHost -Status online $results = $hosts | Invoke-FleetQuery -Query "SELECT * FROM users WHERE uid = '501';" Gets all online hosts and runs a query to find user with UID 501, returning results .EXAMPLE Invoke-FleetQuery -Query "SELECT * FROM processes WHERE name = 'chrome';" -Label "production" Runs a query on all hosts with the "production" label (returns campaign info only) .EXAMPLE Invoke-FleetQuery -QueryId 42 -HostId 100,101,102 -Wait Executes saved query #42 on specific hosts and waits for results .EXAMPLE @(1,2,3,4,5) | Invoke-FleetQuery -Query "SELECT * FROM os_version;" Pipes host IDs to run OS version query .LINK https://fleetdm.com/docs/using-fleet/rest-api#run-live-query #> [CmdletBinding(DefaultParameterSetName = 'Query')] param( [Parameter(ParameterSetName = 'Query', Mandatory)] [ValidateNotNullOrEmpty()] [string]$Query, [Parameter(ParameterSetName = 'QueryId', Mandatory)] [ValidateRange(1, [int]::MaxValue)] [int]$QueryId, [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)] [Alias('Id')] [int[]]$HostId, [string[]]$Label, [switch]$All, [switch]$Wait, [ValidateRange(1, 300)] [int]$MaxWaitTime = 25 ) begin { $hostIds = @() $hasTargets = $false } process { # Collect host IDs from pipeline if ($HostId) { $hostIds += $HostId $hasTargets = $true } } end { # Validate that we have at least one target if (-not $hasTargets -and -not $Label -and -not $All) { throw "You must specify at least one target: -HostId, -Label, or -All" } # For ad-hoc queries with specific host IDs, use the temporary query approach if ($PSCmdlet.ParameterSetName -eq 'Query' -and $hostIds.Count -gt 0 -and -not $Label -and -not $All) { Write-Verbose "Creating temporary query for direct results" $tempQueryName = "TempQuery_$(Get-Date -Format 'yyyyMMdd_HHmmss')_$(Get-Random -Maximum 9999)" $tempQuery = $null try { # Create temporary query Write-Verbose "Creating temporary query: $tempQueryName" $createBody = @{ name = $tempQueryName query = $Query description = "Temporary query created by Invoke-FleetQuery" observer_can_run = $false } $tempQuery = Invoke-FleetDMRequest -Endpoint 'queries' -Method POST -Body $createBody if (-not $tempQuery.query -or -not $tempQuery.query.id) { throw "Failed to create temporary query" } Write-Verbose "Temporary query created with ID: $($tempQuery.query.id)" # Run the query and get results $results = Invoke-FleetSavedQuery -QueryId $tempQuery.query.id -HostId $hostIds return $results } catch { Write-Error "Failed to execute query: $_" throw } finally { # Clean up temporary query if ($tempQuery -and $tempQuery.query.id) { try { Write-Verbose "Cleaning up temporary query ID: $($tempQuery.query.id)" $null = Invoke-FleetDMRequest -Endpoint "queries/$($tempQuery.query.id)" -Method DELETE Write-Verbose "Temporary query cleaned up successfully" } catch { # Only warn if it's not a "not found" error (query might have been auto-deleted) if ($_.Exception.Message -notlike "*not found*" -and $_.Exception.Message -notlike "*does not exist*") { Write-Warning "Failed to clean up temporary query: $_" } else { Write-Verbose "Temporary query already deleted (auto-cleanup)" } } } } } # For saved queries with host IDs and Wait, use the direct result endpoint if ($PSCmdlet.ParameterSetName -eq 'QueryId' -and $Wait -and $hostIds.Count -gt 0 -and -not $Label -and -not $All) { Write-Verbose "Using direct result endpoint for saved query $QueryId" # Redirect to Invoke-FleetSavedQuery for direct results return Invoke-FleetSavedQuery -QueryId $QueryId -HostId $hostIds } # For labels, or -All, we need to use the campaign approach # as the direct results endpoint only works with specific host IDs if ($Label -or $All) { Write-Warning "Direct results are not available when using -Label or -All parameters. Campaign will be started instead." } # Build the request body $body = @{ selected = @{ hosts = $hostIds labels = if ($Label) { $Label } else { @() } } } # Add query or query_id if ($PSCmdlet.ParameterSetName -eq 'Query') { $body['query'] = $Query Write-Verbose "Executing ad-hoc query via campaign: $Query" } else { $body['query_id'] = $QueryId Write-Verbose "Executing saved query ID via campaign: $QueryId" } # Handle -All parameter if ($All) { # FleetDM API doesn't support running ad-hoc queries on all hosts # You must specify at least one host, team, or label Write-Error "The FleetDM API does not support running ad-hoc queries on all hosts without specifying targets. Please specify host IDs, team IDs, or labels." return } if ($body.selected) { Write-Verbose "Query targets: $($body.selected | ConvertTo-Json -Compress)" } try { # Execute the query $result = Invoke-FleetDMRequest -Endpoint 'queries/run' -Method POST -Body $body # Add custom type for formatting $result.PSObject.TypeNames.Insert(0, 'FleetDM.QueryExecution') # Get campaign ID from response $campaignId = $result.campaign.id if (-not $campaignId) { Write-Warning "No campaign ID returned. Query may not have been executed." return $result } Write-Verbose "Query campaign started with ID: $campaignId" if ($Wait) { Write-Host "Waiting for query results..." -NoNewline $startTime = Get-Date $dots = 0 while ((Get-Date) -lt $startTime.AddSeconds($MaxWaitTime)) { Start-Sleep -Seconds 2 # Show progress dots if ($dots -lt 10) { Write-Host "." -NoNewline $dots++ } else { Write-Host "`b`b`b`b`b`b`b`b`b`b `b`b`b`b`b`b`b`b`b`b" -NoNewline $dots = 0 } # Check campaign status try { $campaign = Invoke-FleetDMRequest -Endpoint "queries/results/$campaignId" -Method GET if ($campaign.campaign.status -eq 'finished') { Write-Host " Done!" -ForegroundColor Green # Create result object with campaign results $finalResult = [PSCustomObject]@{ PSTypeName = 'FleetDM.QueryResult' Campaign = $campaign.campaign Results = $campaign.results Errors = $campaign.errors Stats = @{ TotalHosts = $campaign.campaign.totals.count OnlineHosts = $campaign.campaign.totals.online OfflineHosts = $campaign.campaign.totals.offline MissingInActionHosts = $campaign.campaign.totals.missing_in_action ResultsReceived = $campaign.results.Count } } return $finalResult } } catch { # Ignore errors while polling Write-Verbose "Error checking campaign status: $_" } } Write-Host " Timeout!" -ForegroundColor Yellow Write-Warning "Query execution timed out after $MaxWaitTime seconds. Campaign ID: $campaignId" # Try to get partial results try { $campaign = Invoke-FleetDMRequest -Endpoint "queries/results/$campaignId" -Method GET $partialResult = [PSCustomObject]@{ PSTypeName = 'FleetDM.QueryResult' Campaign = $campaign.campaign Results = $campaign.results Errors = $campaign.errors Stats = @{ TotalHosts = $campaign.campaign.totals.count OnlineHosts = $campaign.campaign.totals.online OfflineHosts = $campaign.campaign.totals.offline MissingInActionHosts = $campaign.campaign.totals.missing_in_action ResultsReceived = $campaign.results.Count } Partial = $true } Write-Warning "Returning partial results. Query may still be running." return $partialResult } catch { Write-Error "Failed to retrieve partial results: $_" } } else { # Return campaign info without waiting Write-Host "Query campaign started. Campaign ID: $campaignId" -ForegroundColor Green Write-Host "Query executed successfully. Check FleetDM UI for results." -ForegroundColor Cyan return $result } } catch { throw $_ } } } |