cloud security

Performing Cloud Forensics with AWS CloudTrail

Perform forensic investigation of AWS environments using CloudTrail logs to reconstruct attacker activity, identify compromised credentials, and analyze API call patterns.

awsboto3cloud-securitycloudtraildfirforensicsincident-responses3
Install this skill
npx skills add mukul975/Anthropic-Cybersecurity-Skills
Framework mappings

When to Use

  • When investigating suspected AWS account compromise
  • After detecting unauthorized API calls or credential exposure
  • During incident response involving cloud infrastructure
  • When analyzing S3 data exfiltration or IAM privilege escalation
  • For post-incident forensic timeline reconstruction

Prerequisites

  • AWS account with CloudTrail enabled (management and data events)
  • IAM permissions for cloudtrail:LookupEvents, s3:GetObject, athena:StartQueryExecution
  • boto3 Python SDK installed
  • CloudTrail logs delivered to S3 with optional Athena table configured
  • AWS CLI configured with appropriate credentials

Workflow

  1. Scope Investigation: Identify timeframe, affected accounts, and compromised credentials.
  2. Query CloudTrail: Use boto3 lookup_events or Athena to retrieve relevant API events.
  3. Filter by Indicators: Search for suspicious user agents, source IPs, and event names.
  4. Reconstruct Timeline: Build chronological sequence of attacker actions from API calls.
  5. Analyze Access Patterns: Identify data access, IAM changes, and resource modifications.
  6. Identify Persistence: Check for new IAM users, access keys, roles, or Lambda functions.
  7. Generate Report: Produce forensic timeline with findings and remediation steps.

Key Concepts

Concept Description
LookupEvents CloudTrail API to query management events (last 90 days)
Athena Queries SQL queries against CloudTrail logs in S3 for historical analysis
User Agent Analysis Identify tool signatures (AWS CLI, SDK, console, custom)
AccessKeyId Track activity by specific IAM access key
EventName AWS API action name (e.g., GetObject, CreateUser, AssumeRole)
sourceIPAddress Origin IP of API call for geolocation analysis

Tools & Systems

Tool Purpose
boto3 CloudTrail client Programmatic CloudTrail event lookup
AWS Athena SQL-based analysis of CloudTrail S3 logs
AWS CLI Command-line CloudTrail queries
jq JSON processing for CloudTrail event parsing
CloudTrail Lake Advanced event data store with SQL query support

Output Format

Forensic Report: AWS-IR-[DATE]-[SEQ]
Account: [AWS Account ID]
Timeframe: [Start] to [End]
Compromised Credentials: [Access Key IDs]
Suspicious Events: [Count]
Source IPs: [List of attacker IPs]
Actions Taken: [API calls by attacker]
Data Accessed: [S3 objects, secrets, etc.]
Persistence Mechanisms: [New users, keys, roles]
Source materials

References and resources

Everything below is rendered for inspection. Script files are read-only and never run.

References 1

api-reference.md3.2 KB

AWS CloudTrail Forensics API Reference

boto3 CloudTrail Client

import boto3
client = boto3.client("cloudtrail", region_name="us-east-1")

lookup_events

response = client.lookup_events(
    LookupAttributes=[
        {"AttributeKey": "Username", "AttributeValue": "compromised-user"},
    ],
    StartTime=datetime(2025, 1, 1),
    EndTime=datetime(2025, 1, 2),
    MaxResults=50,
)

LookupAttributes Keys

AttributeKey Description
EventId Unique event identifier
EventName AWS API action (e.g., CreateUser, GetObject)
ReadOnly true/false for read-only API calls
Username IAM user or role session name
ResourceType AWS resource type (e.g., AWS::S3::Object)
ResourceName Name or ARN of the resource accessed
EventSource AWS service (e.g., iam.amazonaws.com)
AccessKeyId IAM access key used for the API call

Response Structure

{
    "Events": [
        {
            "EventId": "abc123",
            "EventName": "CreateUser",
            "EventTime": "2025-01-01T12:00:00Z",
            "Username": "attacker",
            "CloudTrailEvent": "{\"sourceIPAddress\":\"1.2.3.4\",\"userAgent\":\"aws-cli/2.0\",...}"
        }
    ],
    "NextToken": "..."
}

Paginator Usage

paginator = client.get_paginator("lookup_events")
for page in paginator.paginate(
    LookupAttributes=[{"AttributeKey": "AccessKeyId", "AttributeValue": "AKIA..."}],
    StartTime=start, EndTime=end
):
    for event in page["Events"]:
        ct = json.loads(event["CloudTrailEvent"])
        print(ct["sourceIPAddress"], ct["eventName"])

AWS CLI Equivalents

# Lookup events by username
aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=Username,AttributeValue=compromised-user \
  --start-time 2025-01-01T00:00:00Z \
  --output json
 
# Search by access key
aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=AccessKeyId,AttributeValue=AKIAEXAMPLE \
  --max-results 50

Athena Query for S3 CloudTrail Logs

SELECT eventtime, eventname, useridentity.arn, sourceipaddress, useragent,
       requestparameters, responseelements, errorcode
FROM cloudtrail_logs
WHERE eventtime BETWEEN '2025-01-01' AND '2025-01-02'
  AND useridentity.accesskeyid = 'AKIAEXAMPLE'
ORDER BY eventtime;

Key Forensic Event Names

Event Name Service Forensic Significance
CreateUser IAM Persistence - new user account
CreateAccessKey IAM Persistence - new credential
AssumeRole STS Lateral movement / privilege escalation
GetObject S3 Data exfiltration
StopLogging CloudTrail Anti-forensics
PutBucketPolicy S3 Permission modification
RunInstances EC2 Cryptomining / C2 infrastructure
GetSecretValue SecretsManager Credential theft

Suspicious User Agents

User Agent Pattern Tool
Pacu/... AWS exploitation framework
python-requests Custom Python scripts
aws-cli/2.x from unusual IP CLI from attacker machine
Scout Suite Cloud security assessment
Prowler AWS security scanner

Scripts 1

agent.py8.6 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
"""AWS CloudTrail Forensics Agent - investigates API activity for incident response using boto3."""

import json
import argparse
import logging
from collections import defaultdict
from datetime import datetime, timedelta

logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger(__name__)

try:
    import boto3
    HAS_BOTO3 = True
except ImportError:
    HAS_BOTO3 = False

SUSPICIOUS_EVENTS = [
    "CreateUser", "CreateAccessKey", "AttachUserPolicy", "AttachRolePolicy",
    "PutUserPolicy", "CreateRole", "AssumeRole", "CreateLoginProfile",
    "UpdateLoginProfile", "CreateFunction20150331", "UpdateFunctionCode20150331v2",
    "AuthorizeSecurityGroupIngress", "RunInstances", "StopLogging", "DeleteTrail",
    "PutBucketPolicy", "PutBucketAcl", "GetSecretValue", "GetParametersByPath",
]

PERSISTENCE_EVENTS = [
    "CreateUser", "CreateAccessKey", "CreateRole", "CreateLoginProfile",
    "CreateFunction20150331", "CreateEventSourceMapping20150331",
]


def lookup_events(client, start_time, end_time, username=None, access_key_id=None, event_name=None):
    """Query CloudTrail using lookup_events with pagination."""
    kwargs = {
        "StartTime": start_time,
        "EndTime": end_time,
        "MaxResults": 50,
    }
    lookup_attrs = []
    if username:
        lookup_attrs.append({"AttributeKey": "Username", "AttributeValue": username})
    if access_key_id:
        lookup_attrs.append({"AttributeKey": "AccessKeyId", "AttributeValue": access_key_id})
    if event_name:
        lookup_attrs.append({"AttributeKey": "EventName", "AttributeValue": event_name})
    if lookup_attrs:
        kwargs["LookupAttributes"] = lookup_attrs

    events = []
    paginator = client.get_paginator("lookup_events")
    for page in paginator.paginate(**kwargs):
        for event in page.get("Events", []):
            ct_event = json.loads(event.get("CloudTrailEvent", "{}"))
            events.append({
                "event_time": str(event.get("EventTime", "")),
                "event_name": event.get("EventName", ""),
                "event_source": ct_event.get("eventSource", ""),
                "username": event.get("Username", ""),
                "source_ip": ct_event.get("sourceIPAddress", ""),
                "user_agent": ct_event.get("userAgent", ""),
                "access_key_id": ct_event.get("userIdentity", {}).get("accessKeyId", ""),
                "arn": ct_event.get("userIdentity", {}).get("arn", ""),
                "error_code": ct_event.get("errorCode", ""),
                "error_message": ct_event.get("errorMessage", ""),
                "request_params": ct_event.get("requestParameters", {}),
                "response_elements": ct_event.get("responseElements", {}),
                "aws_region": ct_event.get("awsRegion", ""),
            })
    logger.info("Retrieved %d CloudTrail events", len(events))
    return events


def detect_suspicious_activity(events):
    """Flag events matching suspicious API calls."""
    suspicious = []
    for event in events:
        if event["event_name"] in SUSPICIOUS_EVENTS:
            event["indicator"] = "suspicious_api_call"
            event["severity"] = "high" if event["event_name"] in PERSISTENCE_EVENTS else "medium"
            suspicious.append(event)
        if event["error_code"] == "AccessDenied":
            event["indicator"] = "access_denied_enumeration"
            event["severity"] = "medium"
            suspicious.append(event)
    return suspicious


def detect_persistence(events):
    """Identify persistence mechanisms created by attacker."""
    persistence = []
    for event in events:
        if event["event_name"] in PERSISTENCE_EVENTS and not event["error_code"]:
            details = {}
            resp = event.get("response_elements", {})
            if event["event_name"] == "CreateUser":
                details["created_user"] = resp.get("user", {}).get("userName", "")
            elif event["event_name"] == "CreateAccessKey":
                details["access_key_id"] = resp.get("accessKey", {}).get("accessKeyId", "")
                details["for_user"] = resp.get("accessKey", {}).get("userName", "")
            elif event["event_name"] == "CreateRole":
                details["role_name"] = resp.get("role", {}).get("roleName", "")
            persistence.append({**event, "persistence_details": details})
    return persistence


def analyze_source_ips(events):
    """Analyze source IP distribution for anomalies."""
    ip_activity = defaultdict(lambda: {"count": 0, "events": set(), "users": set()})
    for event in events:
        ip = event["source_ip"]
        if ip:
            ip_activity[ip]["count"] += 1
            ip_activity[ip]["events"].add(event["event_name"])
            ip_activity[ip]["users"].add(event["username"])
    result = {}
    for ip, data in ip_activity.items():
        result[ip] = {
            "request_count": data["count"],
            "unique_events": len(data["events"]),
            "unique_users": len(data["users"]),
            "event_types": list(data["events"])[:10],
        }
    return dict(sorted(result.items(), key=lambda x: x[1]["request_count"], reverse=True))


def analyze_user_agents(events):
    """Analyze user agents for tool identification."""
    ua_counts = defaultdict(int)
    for event in events:
        ua = event.get("user_agent", "unknown")
        ua_counts[ua] += 1
    suspicious_uas = {}
    for ua, count in ua_counts.items():
        if any(tool in ua.lower() for tool in ["pacu", "prowler", "scoutsuite", "boto", "python", "curl", "custom"]):
            suspicious_uas[ua] = count
    return {
        "all_user_agents": dict(sorted(ua_counts.items(), key=lambda x: x[1], reverse=True)[:15]),
        "suspicious_user_agents": suspicious_uas,
    }


def build_timeline(events):
    """Build chronological attack timeline."""
    return sorted(
        [{"time": e["event_time"], "event": e["event_name"], "user": e["username"],
          "source_ip": e["source_ip"], "error": e.get("error_code", "")}
         for e in events],
        key=lambda x: x["time"]
    )


def generate_report(events, suspicious, persistence, ip_analysis, ua_analysis):
    """Generate forensic investigation report."""
    report = {
        "timestamp": datetime.utcnow().isoformat(),
        "investigation_type": "AWS CloudTrail Forensic Analysis",
        "total_events_analyzed": len(events),
        "suspicious_events": len(suspicious),
        "persistence_mechanisms_found": len(persistence),
        "unique_source_ips": len(ip_analysis),
        "source_ip_analysis": dict(list(ip_analysis.items())[:10]),
        "user_agent_analysis": ua_analysis,
        "persistence_details": persistence[:10],
        "top_suspicious_events": suspicious[:20],
        "timeline": build_timeline(events)[:50],
    }
    return report


def main():
    parser = argparse.ArgumentParser(description="AWS CloudTrail Forensics Agent")
    parser.add_argument("--hours-back", type=int, default=24, help="Hours to look back (default: 24)")
    parser.add_argument("--username", help="Filter by IAM username")
    parser.add_argument("--access-key-id", help="Filter by access key ID")
    parser.add_argument("--event-name", help="Filter by specific event name")
    parser.add_argument("--region", default="us-east-1", help="AWS region (default: us-east-1)")
    parser.add_argument("--profile", help="AWS CLI profile name")
    parser.add_argument("--output", default="cloudtrail_forensics_report.json")
    args = parser.parse_args()

    if not HAS_BOTO3:
        logger.error("boto3 is required: pip install boto3")
        return

    session_kwargs = {}
    if args.profile:
        session_kwargs["profile_name"] = args.profile
    session = boto3.Session(**session_kwargs)
    client = session.client("cloudtrail", region_name=args.region)

    end_time = datetime.utcnow()
    start_time = end_time - timedelta(hours=args.hours_back)
    logger.info("Querying CloudTrail: %s to %s", start_time.isoformat(), end_time.isoformat())

    events = lookup_events(client, start_time, end_time, args.username, args.access_key_id, args.event_name)
    suspicious = detect_suspicious_activity(events)
    persistence = detect_persistence(events)
    ip_analysis = analyze_source_ips(events)
    ua_analysis = analyze_user_agents(events)

    report = generate_report(events, suspicious, persistence, ip_analysis, ua_analysis)
    with open(args.output, "w") as f:
        json.dump(report, f, indent=2, default=str)
    logger.info("Forensics: %d events, %d suspicious, %d persistence mechanisms",
                len(events), len(suspicious), len(persistence))
    print(json.dumps(report, indent=2, default=str))


if __name__ == "__main__":
    main()
Keep exploring