Tools · AWS detection and investigation · 03 / 04
CloudTrail Lake: create an event data store and answer questions in SQL
Create a CloudTrail Lake event data store in a sandbox account, run SQL queries with the AWS CLI, and read the results to see who made failed calls and which principals created access keys.
SQL over CloudTrail without a pipeline
CloudTrail Lake stores events in an event data store and lets you query them with SQL, without exporting logs to S3 and setting up Athena. It bills for ingestion and for data scanned, so narrow every query by time. Check current availability and pricing for your account and Region before creating a store.
aws cloudtrail create-event-data-store --name sandbox-mgmt \ --retention-period 90 --no-multi-region-enabled aws cloudtrail list-event-data-stores --query 'EventDataStores[].[Name,EventDataStoreArn]' --output text
Running a query
The FROM clause names the store by its ID, the last part of its ARN. start-query returns a query ID; get-query-results returns rows once the status is FINISHED.
EDS=a1b2c3d4-5678-90ab-cdef-EXAMPLE11111 QID=$(aws cloudtrail start-query --query-statement \ "SELECT eventTime, eventName, userIdentity.arn, sourceIPAddress, errorCode FROM $EDS WHERE eventTime > '2026-11-01 00:00:00' AND errorCode = 'AccessDenied' ORDER BY eventTime DESC LIMIT 20" --query QueryId --output text) aws cloudtrail get-query-results --query-id "$QID" --output json
Reading the rows
Results come back as lists of single-key objects. A short jq expression flattens them into a table.
aws cloudtrail get-query-results --query-id "$QID" | jq -r '.QueryResultRows[] | map(to_entries[0].value) | @tsv' 2026-11-02 03:12:40 ListBuckets arn:aws:sts::111122223333:assumed-role/app-web-role/i-0abc 198.51.100.200 AccessDenied 2026-11-02 03:12:41 GetBucketPolicy arn:aws:sts::111122223333:assumed-role/app-web-role/i-0abc 198.51.100.200 AccessDenied
Worked question: who creates access keys?
Long-lived access keys are a common source of leaks. This query lists every CreateAccessKey call in the window with the creator and the user the key was created for. In the sandbox it returns two rows: one from the platform team’s automation, expected, and one from a developer creating a key for themselves at 23:40, which goes to their manager with the account policy that discourages user keys.
SELECT eventTime, userIdentity.arn AS creator,
element_at(requestParameters, 'userName') AS forUser, sourceIPAddress
FROM <eds-id>
WHERE eventName = 'CreateAccessKey' AND eventTime > '2026-10-01 00:00:00'
ORDER BY eventTimeDiagram
A CloudTrail Lake question in four steps
- Store
Create an event data store with a retention period.
aws cloudtrail create-event-data-store --name sandbox-mgmt --retention-period 90
- Ask
Start a SQL query bounded by time.
aws cloudtrail start-query --query-statement "SELECT ... WHERE eventTime > '2026-11-01 00:00:00'"
- Fetch
Collect rows when the status is FINISHED.
aws cloudtrail get-query-results --query-id "$QID"
- Flatten
Turn rows into a readable table.
jq -r '.QueryResultRows[] | map(to_entries[0].value) | @tsv'
Check yourself
1. Why bound every CloudTrail Lake query by eventTime?
2. Which value goes in the FROM clause?
3. How do you read a key from requestParameters in Lake SQL?