Query Console¶
Query and analyze your security telemetry using LimaCharlie Query Language (LCQL).
Documentation¶
- LCQL Examples - Example queries for common use cases
- Query Console UI - Using the web-based query interface
- Query with CLI - Running queries from the command line
- Query Limits & Performance - Concurrency and timeout limits, and how to write efficient queries
Running Queries Programmatically¶
Prerequisites
All API examples require an API key with the insight permission. See API Keys for setup.
Search API Endpoint¶
There is no single search hostname. Each organization's search endpoint lives in the datacenter for the region where the organization was created, so you discover it from the API first and then send queries to that host. The Python SDK, Go SDK, and CLI do this automatically.
The Python SDK resolves the search endpoint automatically from your OID; no manual step is required.
The CLI resolves the search endpoint automatically from your OID; no manual step is required.
The bootstrap API host https://api.limacharlie.io is the same for every region and routes to the correct datacenter based on your OID. The REST examples below reuse the $SEARCH_HOST variable for the discovered hostname. For reference, the current production search endpoints per region are:
| Region | Search endpoint |
|---|---|
| USA | https://9157798c50af372c.replay-search.limacharlie.io |
| Europe | https://b76093c3662d5b4f.replay-search.limacharlie.io |
| Canada | https://aae67d7e76570ec1.replay-search.limacharlie.io |
| UK | https://70182cf634c346bd.replay-search.limacharlie.io |
| Australia | https://abc32764762fce67.replay-search.limacharlie.io |
| India | https://4d897015b0815621.replay-search.limacharlie.io |
Use the bootstrap API, not a direct endpoint
Discovering your search endpoint through the bootstrap API (https://api.limacharlie.io/v1/orgs/{oid}/url, the url.search field) is the recommended approach. The direct per-region hostnames above can change over time, whereas the bootstrap API always returns the current endpoint for your organization. Treat the table as a convenience reference only, and do not hardcode a direct endpoint.
Run an LCQL Query¶
# Start and end times are unix epoch seconds
START=$(date -d '1 hour ago' +%s)
END=$(date +%s)
# Discover your org's search endpoint (see "Search API Endpoint" above)
SEARCH_HOST=$(curl -s "https://api.limacharlie.io/v1/orgs/YOUR_OID/url" \
-H "Authorization: Bearer $LC_JWT" | jq -r '.url.search')
curl -s -X POST \
"https://$SEARCH_HOST/v1/search" \
-H "Authorization: Bearer $LC_JWT" \
-H "Content-Type: application/json" \
-d '{
"oid": "YOUR_OID",
"query": "event/FILE_PATH ends with .exe",
"startTime": "'"$START"'",
"endTime": "'"$END"'",
"stream": "event"
}'
import time
from limacharlie.client import Client
from limacharlie.sdk.organization import Organization
from limacharlie.sdk.search import Search
client = Client(oid="YOUR_OID", api_key="YOUR_API_KEY")
org = Organization(client)
end = int(time.time())
start = end - 3600 # 1 hour ago
for result in Search(org).execute(
query="event/FILE_PATH ends with .exe",
start_time=start,
end_time=end,
stream="event",
limit=100,
):
print(result)
package main
import (
"fmt"
limacharlie "github.com/refractionPOINT/go-limacharlie/limacharlie"
)
func main() {
org, err := limacharlie.NewOrganizationFromClientOptions(limacharlie.ClientOptions{
OID: "YOUR_OID",
APIKey: "YOUR_API_KEY",
}, nil)
if err != nil {
panic(err)
}
// The Go SDK's Query takes the full LCQL string (time range included).
resp, err := org.Query(limacharlie.QueryRequest{
Query: "-1h | * | * | event/FILE_PATH ends with '.exe'",
Stream: "event",
LimitEvent: 1000,
})
if err != nil {
panic(err)
}
for _, r := range resp.Results {
fmt.Println(r)
}
}
Validate Query Syntax¶
package main
import (
"fmt"
limacharlie "github.com/refractionPOINT/go-limacharlie/limacharlie"
)
func main() {
org, err := limacharlie.NewOrganizationFromClientOptions(limacharlie.ClientOptions{
OID: "YOUR_OID",
APIKey: "YOUR_API_KEY",
}, nil)
if err != nil {
panic(err)
}
result, err := org.ValidateLCQLQuery("-1h | * | * | event/FILE_PATH ends with '.exe'")
if err != nil {
panic(err)
}
if result.Error != "" {
fmt.Printf("invalid query: %s\n", result.Error)
}
}
Estimate Query Cost¶
START=$(date -d '1 hour ago' +%s)
END=$(date +%s)
curl -s -X POST \
"https://$SEARCH_HOST/v1/search/validate" \
-H "Authorization: Bearer $LC_JWT" \
-H "Content-Type: application/json" \
-d '{
"oid": "YOUR_OID",
"query": "event/FILE_PATH ends with .exe",
"startTime": "'"$START"'",
"endTime": "'"$END"'"
}'
import time
from limacharlie.client import Client
from limacharlie.sdk.organization import Organization
from limacharlie.sdk.search import Search
client = Client(oid="YOUR_OID", api_key="YOUR_API_KEY")
org = Organization(client)
end = int(time.time())
start = end - 3600
result = Search(org).estimate(
query="event/FILE_PATH ends with .exe",
start_time=start,
end_time=end,
)
print(result)
package main
import (
"fmt"
limacharlie "github.com/refractionPOINT/go-limacharlie/limacharlie"
)
func main() {
org, err := limacharlie.NewOrganizationFromClientOptions(limacharlie.ClientOptions{
OID: "YOUR_OID",
APIKey: "YOUR_API_KEY",
}, nil)
if err != nil {
panic(err)
}
est, err := org.EstimateLCQLQueryBilling("-1h | * | * | event/FILE_PATH ends with '.exe'")
if err != nil {
panic(err)
}
fmt.Printf("billed events: %d, estimated price: %.2f %s\n",
est.BilledEvents, est.EstimatedPrice.Price, est.EstimatedPrice.Currency)
}
Note
The validate response also includes query-size fields (batchesInScope, eventsInScope, bytesInScope) and a running search returns per-page progress and actual billing. See Query Progress and Cost Reporting for how to build a progress bar and read the real cost.
Saved Queries¶
List Saved Queries¶
package main
import (
"fmt"
limacharlie "github.com/refractionPOINT/go-limacharlie/limacharlie"
)
func main() {
org, err := limacharlie.NewOrganizationFromClientOptions(limacharlie.ClientOptions{
OID: "YOUR_OID",
APIKey: "YOUR_API_KEY",
}, nil)
if err != nil {
panic(err)
}
hive := limacharlie.NewHiveClient(org)
queries, err := hive.List(limacharlie.HiveArgs{
HiveName: "query",
PartitionKey: "YOUR_OID",
})
if err != nil {
panic(err)
}
fmt.Println(queries)
}
Create Saved Query¶
from limacharlie.client import Client
from limacharlie.sdk.organization import Organization
from limacharlie.sdk.hive import Hive, HiveRecord
client = Client(oid="YOUR_OID", api_key="YOUR_API_KEY")
org = Organization(client)
Hive(org, "query").set(HiveRecord(
name="my-saved-query",
data={
"query": "event/FILE_PATH ends with .exe",
"stream": "event",
},
enabled=True,
))
package main
import (
limacharlie "github.com/refractionPOINT/go-limacharlie/limacharlie"
)
func main() {
org, err := limacharlie.NewOrganizationFromClientOptions(limacharlie.ClientOptions{
OID: "YOUR_OID",
APIKey: "YOUR_API_KEY",
}, nil)
if err != nil {
panic(err)
}
hive := limacharlie.NewHiveClient(org)
enabled := true
if _, err := hive.Add(limacharlie.HiveArgs{
HiveName: "query",
PartitionKey: "YOUR_OID",
Key: "my-saved-query",
Data: limacharlie.Dict{
"query": "event/FILE_PATH ends with .exe",
"stream": "event",
},
Enabled: &enabled,
}); err != nil {
panic(err)
}
}
Run Saved Query¶
import time
from limacharlie.client import Client
from limacharlie.sdk.organization import Organization
from limacharlie.sdk.hive import Hive
from limacharlie.sdk.search import Search
client = Client(oid="YOUR_OID", api_key="YOUR_API_KEY")
org = Organization(client)
saved = Hive(org, "query").get("my-saved-query")
end = int(time.time())
start = end - 3600
for result in Search(org).execute(
query=saved.data["query"],
start_time=start,
end_time=end,
stream=saved.data.get("stream", "event"),
):
print(result)
package main
import (
"fmt"
limacharlie "github.com/refractionPOINT/go-limacharlie/limacharlie"
)
func main() {
org, err := limacharlie.NewOrganizationFromClientOptions(limacharlie.ClientOptions{
OID: "YOUR_OID",
APIKey: "YOUR_API_KEY",
}, nil)
if err != nil {
panic(err)
}
hive := limacharlie.NewHiveClient(org)
saved, err := hive.Get(limacharlie.HiveArgs{
HiveName: "query",
PartitionKey: "YOUR_OID",
Key: "my-saved-query",
})
if err != nil {
panic(err)
}
resp, err := org.Query(limacharlie.QueryRequest{
Query: saved.Data["query"].(string),
Stream: "event",
})
if err != nil {
panic(err)
}
for _, r := range resp.Results {
fmt.Println(r)
}
}
Delete Saved Query¶
package main
import (
limacharlie "github.com/refractionPOINT/go-limacharlie/limacharlie"
)
func main() {
org, err := limacharlie.NewOrganizationFromClientOptions(limacharlie.ClientOptions{
OID: "YOUR_OID",
APIKey: "YOUR_API_KEY",
}, nil)
if err != nil {
panic(err)
}
hive := limacharlie.NewHiveClient(org)
if _, err := hive.Remove(limacharlie.HiveArgs{
HiveName: "query",
PartitionKey: "YOUR_OID",
Key: "my-saved-query",
}); err != nil {
panic(err)
}
}