AWS CloudTrail Investigation Guide: From Suspicious Alert to Full Scope in 60 Minutes
AWS CloudTrail records every API call made in your account, including who made it, from where, what they did, and what the response was. When a GuardDuty finding or suspicious activity triggers an investigation, CloudTrail is the forensic record that lets you answer: what did this principal do, when did it start, and what is the full scope of the activity? These queries build a complete timeline from a single suspicious event.
Investigation Setup -- Verify You Have Comprehensive Logging
Before querying, confirm your logging baseline:
Check if a trail exists:
aws cloudtrail describe-trails --include-shadow-trails
Verify the trail is actively logging:
aws cloudtrail get-trail-status --name [trail-name]
Key fields to check in the output:
IsLogging: true-- trail is activeIsMultiRegionTrail: true-- required to catch cross-region activity; a single-region trail misses activity in other regionsHasCustomEventSelectorsorHasInsightSelectors-- confirms data events and/or Insights are enabled
If no trail exists and you need one immediately:
aws cloudtrail create-trail \
--name incident-trail \
--s3-bucket-name [your-bucket] \
--is-multi-region-trail
aws cloudtrail start-logging --name incident-trail
Note: a new trail only captures events going forward. For historical events, you are limited to Event History (90 days) unless a prior trail was writing to S3.
Starting Point -- Triaging a GuardDuty Finding
Retrieve the full GuardDuty finding detail:
aws guardduty get-findings \
--detector-id [id] \
--finding-ids [finding-id] \
| jq '.Findings[0]'
Extract the three primary pivot values from the finding:
- Principal ARN (who acted):
jq '.Findings[0].Resource.AccessKeyDetails.UserName'
# or
jq '.Findings[0].Resource.AccessKeyDetails.PrincipalId'
- Source IP (where it came from):
jq '.Findings[0].Service.Action.AwsApiCallAction.RemoteIpDetails.IpAddressV4'
- The triggering API call (what they did):
jq '.Findings[0].Service.Action.AwsApiCallAction.Api'
These three values -- principal ARN, source IP, and triggering API call -- are the starting pivot points for everything that follows.
Briefings like this, every morning before 9am.
Threat intel, active CVEs, and campaign alerts, distilled for practitioners. 50,000+ subscribers. No noise.
Pivoting by Principal -- All Actions from a Role or User
Quick lookup via CLI (90-day window, single attribute at a time):
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=Username,AttributeValue=[role-session-name] \
--start-time 2026-05-13T00:00:00Z \
--max-results 50 \
| jq '.Events[] | {time: .EventTime, event: .EventName, region: (.CloudTrailEvent | fromjson | .awsRegion)}'
Full history via S3 trail with Athena (covers all time, all regions, all event types):
SELECT eventtime, eventsource, eventname, sourceipaddress,
json_extract_scalar(useridentity, '$.arn') AS principal_arn,
requestparameters
FROM cloudtrail_logs
WHERE year='2026' AND month='05'
AND json_extract_scalar(useridentity, '$.sessionContext.sessionIssuer.arn') = '[role-arn]'
ORDER BY eventtime DESC
LIMIT 500;
The sessionContext.sessionIssuer.arn filter catches all assumed-role sessions from the compromised role, even if the session names differ across invocations.
Pivoting by IP -- All Principals from a Source IP
Determine whether multiple principals were used from the same suspicious IP:
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=SourceIPAddress,AttributeValue=[suspicious-ip] \
| jq '.Events[] | {time: .EventTime, principal: (.CloudTrailEvent | fromjson | .userIdentity.arn), event: .EventName}'
Athena equivalent:
SELECT DISTINCT json_extract_scalar(useridentity, '$.arn') AS principal,
COUNT(*) AS event_count
FROM cloudtrail_logs
WHERE sourceipaddress = '[suspicious-ip]'
AND year='2026' AND month='05'
GROUP BY 1
ORDER BY 2 DESC;
If the suspicious IP is an internal address, cross-reference with VPC Flow Logs:
aws logs filter-log-events \
--log-group-name [vpc-flow-log-group] \
--filter-pattern "[version, account, eni, source=[suspicious-ip], ...]" \
--start-time [epoch-ms]
High-Risk Event Patterns to Look For
Categorize the activity you find by attacker objective:
Credential persistence (attacker creating durable access):
CreateAccessKey,CreateLoginProfile,UpdateLoginProfile
Privilege escalation:
AttachUserPolicy,AttachRolePolicy,PutUserPolicy,CreateRole,PassRole
Reconnaissance:
ListBuckets,ListFunctions,DescribeInstances,GetAccountSummary,ListRoles
Data exfiltration:
GetObject(S3),GetSecretValue(Secrets Manager),GetParameter(SSM Parameter Store),Decrypt(KMS)
Covering tracks:
DeleteTrail,StopLogging,DeleteLogGroup,DeleteFlowLogs
jq filter to isolate high-risk events from a downloaded CloudTrail export:
cat cloudtrail-export.json | jq '[
.Records[] |
select(.eventName | test(
"CreateAccessKey|AttachUserPolicy|PutUserPolicy|CreateRole|GetSecretValue|DeleteTrail|StopLogging"
))
] | sort_by(.eventTime)'
Building the Timeline
Sort all events chronologically and extract the key fields:
cat cloudtrail-export.json | jq '[
.Records[]
] | sort_by(.eventTime) | .[] | {
time: .eventTime,
action: .eventName,
principal: .userIdentity.arn,
region: .awsRegion,
sourceIP: .sourceIPAddress,
params: .requestParameters
}'
Analysis objectives for the timeline:
-
Initial access event: the first API call from the suspicious principal or IP. This is the breach point. Check what credential was used and whether there was a ConsoleLogin or AssumeRole event immediately before it.
-
Persistence mechanisms: any CreateAccessKey, CreateUser, or role modification after initial access. These indicate the attacker planned to return.
-
Blast radius: all resource types accessed, all regions where activity occurred, any data events (S3 GetObject at scale indicates exfiltration).
Timeline template columns for documentation and escalation:
| Timestamp (UTC) | EventName | Principal ARN | Source IP | Region | Resource Type | Resource ARN | Notes |
The bottom line
CloudTrail investigation is a pivot exercise. Start with the three values from the triggering alert (principal, IP, event name), then systematically expand: all actions by that principal, all principals from that IP, all high-risk event types in the timeframe. The goal in the first 60 minutes is not to understand everything -- it is to bound the scope so you know whether you are dealing with a single compromised key or a broader intrusion.
Frequently asked questions
What is the difference between CloudTrail Event History and a CloudTrail trail?
Event History is the free 90-day lookup UI in the console. A trail is a configuration that writes all events to an S3 bucket (and optionally CloudWatch Logs or CloudTrail Lake) for longer retention and structured querying. You need a trail for serious incident investigation.
How do I find all actions taken by a specific IAM role in CloudTrail?
Use `aws cloudtrail lookup-events --lookup-attributes AttributeKey=Username,AttributeValue=[role-name]` for quick lookup. For comprehensive results, query the S3 trail with Athena or use CloudTrail Lake.
What CloudTrail events indicate a credential compromise?
High-risk events: ConsoleLogin (especially from a new IP or region), CreateAccessKey (attacker creating persistence), AssumeRole (lateral movement), GetSecretValue (data exfiltration from Secrets Manager), DescribeInstances followed by RunInstances (reconnaissance then resource deployment).
How do I set up Athena to query CloudTrail logs in S3?
Create an Athena table using the CloudTrail Athena DDL (AWS provides a one-click option in the CloudTrail console). Then query with standard SQL against the table, using partition filters (year, month, day) to control costs.
What is CloudTrail Lake and when should I use it instead of Athena?
CloudTrail Lake is a managed event data store with a SQL query interface. It is simpler to set up than Athena with S3 but costs more per event stored. Use CloudTrail Lake if you want fast setup for incident response; use Athena if you already have S3 trails and want to minimize ongoing cost.
How do I tell if a CloudTrail event came from an assumed role vs. a direct IAM user?
Check `userIdentity.type` in the event. Values: `IAMUser` (direct user), `AssumedRole` (temporary credentials from STS), `Root` (root account), `AWSService` (AWS service action). For `AssumedRole`, `userIdentity.sessionContext.sessionIssuer.arn` shows the underlying role.
Sources & references
Free resources
Critical CVE Reference Card 2025–2026
25 actively exploited vulnerabilities with CVSS scores, exploit status, and patch availability. Print it, pin it, share it with your SOC team.
Ransomware Incident Response Playbook
Step-by-step 24-hour IR checklist covering detection, containment, eradication, and recovery. Built for SOC teams, IR leads, and CISOs.
Get threat intel before your inbox does.
50,000+ security professionals read Decryption Digest for early warnings on zero-days, ransomware, and nation-state campaigns. Free, weekly, no spam.
Unsubscribe anytime. We never sell your data.

Founder & Cybersecurity Evangelist, Decryption Digest
Cybersecurity professional with expertise in threat intelligence, vulnerability research, and enterprise security. Covers zero-days, ransomware, and nation-state operations for 50,000+ security professionals weekly.
The Mythos Brief is free.
AI that finds 27-year-old zero-days. What it means for your security program.
