AWS Digital Forensics Investigation

Trace a credential compromise using GuardDuty, CloudTrail, and Athena.

Introduction

30 Second Summary

When a security breach happens in the cloud, the first question is always "what did the attacker actually do?" Answering that question quickly and accurately is what separates a minor incident from a full-blown crisis.

In this project, you will investigate a simulated IAM credential compromise in AWS by tracing the attacker's steps from a GuardDuty alert through CloudTrail logs queried with SQL in Amazon Athena.

What You'll Build

You will run a complete forensic investigation from detection to final report, proving exactly who did what, when, and from where in your AWS environment.

By the end of this project, you'll have:

  • A triage workflow where a GuardDuty finding triggers your investigation and you extract the compromised credential, source IP, and flagged API calls as forensic starting points.
  • A SQL-powered forensic search using Athena to query CloudTrail logs and uncover the full attack chain beyond what GuardDuty alone can show.
  • A validated forensic report with cryptographically verified evidence proving your logs have not been tampered with, following the NIST four-phase forensic methodology.
  • Secret Mission: Export your forensic investigation results to a persistent Parquet table using Athena's CTAS and register it in the AWS Glue Data Catalog for your team to query.

Are there any prerequisites?

You need an AWS account with GuardDuty already enabled, the AWS CLI installed and configured, and basic familiarity with IAM and S3.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters for cloud security. This is where you commit to the goal so every step that follows has clear purpose.

Set Up the Forensic Evidence Pipeline

To investigate a security incident in AWS, you need tamper-proof evidence. CloudTrail Event History retains only 90 days of management events, and you cannot query it with SQL.

In this step, you will create a dedicated forensic evidence pipeline. A CloudTrail trail delivers every API call to an S3 bucket, with cryptographic log file integrity validation so you can prove the evidence has not been modified.

In this step, get ready to:
  • Create an S3 bucket with a policy granting CloudTrail write access.
  • Create and start a CloudTrail trail with log file integrity validation.
  • Confirm GuardDuty is active and note the detector ID.
Create the forensic evidence bucket

CloudTrail needs an S3 bucket to deliver log files into. You will create a dedicated bucket and attach a policy that grants the CloudTrail service principal permission to write logs.

  • Create your S3 bucket by running this command (replace the bucket name with a globally unique name that includes your name or account ID):
aws s3api create-bucket --bucket [[FORENSICS_BUCKET="your-forensics-bucket-name"]] --region us-east-1

Why include your name in the bucket name?

S3 bucket names must be globally unique across all AWS accounts. Adding your name or a random string (like forensics-trail-yourname-2026) prevents naming collisions.

💡 Not in us-east-1?

If your AWS CLI is configured for a region other than us-east-1, add the --create-bucket-configuration LocationConstraint=your-region flag. Otherwise the bucket defaults to us-east-1.

  • Confirm your bucket was created by running:
aws s3 ls | grep forensics

You should see your bucket name listed with today's date.

Now you need to attach a bucket policy so CloudTrail can deliver logs. The policy grants two permissions: s3:GetBucketAcl (CloudTrail checks it can write) and s3:PutObject (CloudTrail writes the actual log files).

  • Save the following bucket policy as a file called bucket-policy.json on your Desktop. Replace the placeholder values with your actual bucket name, AWS account ID, and region:
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AWSCloudTrailAclCheck20150319",
            "Effect": "Allow",
            "Principal": {"Service": "cloudtrail.amazonaws.com"},
            "Action": "s3:GetBucketAcl",
            "Resource": "arn:aws:s3:::your-forensics-bucket-name",
            "Condition": {
                "StringEquals": {
                    "aws:SourceArn": "arn:aws:cloudtrail:your-region:your-account-id:trail/forensics-trail"
                }
            }
        },
        {
            "Sid": "AWSCloudTrailWrite20150319",
            "Effect": "Allow",
            "Principal": {"Service": "cloudtrail.amazonaws.com"},
            "Action": "s3:PutObject",
            "Resource": "arn:aws:s3:::your-forensics-bucket-name/AWSLogs/your-account-id/*",
            "Condition": {
                "StringEquals": {
                    "s3:x-amz-acl": "bucket-owner-full-control",
                    "aws:SourceArn": "arn:aws:cloudtrail:your-region:your-account-id:trail/forensics-trail"
                }
            }
        }
    ]
}

What does this policy do?

The first statement lets CloudTrail check that it has permission to write to the bucket. The second statement lets CloudTrail put log file objects into the AWSLogs/ prefix inside your bucket.

The aws:SourceArn condition ensures only your specific trail (forensics-trail) can write to this bucket. This is an AWS security best practice.

  • Apply the bucket policy by running:
aws s3api put-bucket-policy --bucket [[FORENSICS_BUCKET="your-forensics-bucket-name"]] --policy file://bucket-policy.json
  • Verify the policy was applied by running:
aws s3api get-bucket-policy --bucket [[FORENSICS_BUCKET="your-forensics-bucket-name"]]

You should see the JSON policy you just applied returned in the output.

Seeing an access denied error?

Make sure your AWS CLI credentials have s3:PutBucketPolicy and s3:GetBucketPolicy permissions. If you are using an IAM user with limited permissions, attach the AmazonS3FullAccess managed policy temporarily.

Also confirm you spelled your bucket name identically in the command and inside the JSON policy file.help me troubleshoot this S3 bucket policy error for CloudTrail

Create the CloudTrail trail with integrity validation

With your bucket ready, you can now create the trail that delivers every API call as a log file. Enabling log file validation means CloudTrail signs each file with SHA-256 hashing and RSA digital signatures. Later, you can prove the logs have not been tampered with.

  • Create the trail by running:
aws cloudtrail create-trail --name forensics-trail --s3-bucket-name [[FORENSICS_BUCKET="your-forensics-bucket-name"]] --is-multi-region-trail --enable-log-file-validation

What do these flags do?

  • --name forensics-trail gives the trail a descriptive name you will reference later.
  • --s3-bucket-name points to the bucket you just created.
  • --is-multi-region-trail records events across all enabled AWS regions, not just your current one.
  • --enable-log-file-validation turns on cryptographic integrity checking for every log file delivered.

You should see JSON output including "LogFileValidationEnabled": true and "IsMultiRegionTrail": true.

  • The trail is created but not yet recording. Start logging by running:
aws cloudtrail start-logging --name forensics-trail

This command returns no output on success. Your trail is now actively recording every API call in your account.

Getting an InsufficientS3BucketPolicyException?

This means CloudTrail cannot write to your bucket. Double check that the aws:SourceArn in your bucket policy exactly matches arn:aws:cloudtrail:your-region:your-account-id:trail/forensics-trail. The region, account ID, and trail name must all be correct.

help me fix an InsufficientS3BucketPolicyException when creating a CloudTrail trail

Confirm GuardDuty is active

GuardDuty is your detection layer. It automatically analyzes CloudTrail management events, VPC Flow Logs, and DNS logs to identify threats. You need to confirm it is running and grab the detector ID for later steps.

  • Retrieve your GuardDuty detector ID by running:
aws guardduty list-detectors

You should see output containing a DetectorIds array with at least one ID string (something like a1b2c3d4e5f6g7h8i9j0).

  • Copy this detector ID and save it somewhere accessible (a sticky note, a text file, or your clipboard). You will need it in the next step.

DetectorIds is empty?

An empty list means GuardDuty is not enabled in your current region. Open the GuardDuty console, select your region, and click Get Started then Enable GuardDuty. Then re-run the command.

help me enable GuardDuty in my AWS region

Before you run the final verification, what do you expect to see when you describe the trail you just created?

  • Verify your full setup by running:
aws cloudtrail describe-trails --trail-name-list forensics-trail

You should see output confirming "LogFileValidationEnabled": true, your S3 bucket name, and "IsMultiRegionTrail": true. This confirms your forensic evidence pipeline is operational.

Your forensic evidence pipeline is live. CloudTrail is now recording every API call in your account and delivering tamper-proof logs to S3. Next up, you will generate suspicious activity to investigate.

Simulate Attack Activity and Trigger Detection

Your forensic evidence pipeline is live. CloudTrail is now delivering every API call in your account to S3 with integrity validation.

But a forensics pipeline is useless without something to investigate. In this step, you will create a test IAM user that acts as a "compromised credential" and generate realistic GuardDuty findings. This gives you both the raw CloudTrail evidence and the detection alerts you need to practice triaging a real incident.

In this step, get ready to:
  • Create a test IAM user with an access key and run reconnaissance API calls.
  • Generate GuardDuty sample findings that simulate a credential compromise detection.
  • Confirm the sample findings are visible in the GuardDuty console.
Create the test IAM user and generate CloudTrail evidence

To simulate a credential compromise, you need an IAM user whose access key an attacker could have stolen. You will create this user, generate an access key, and then use those credentials to make API calls that mimic attacker reconnaissance.

  • Create the test IAM user by running this command:
aws iam create-user --user-name forensics-test-user

What does this command do?

This creates a new IAM user named forensics-test-user in your account. The user has no permissions or console access. It exists purely as a credential identity that you will trace through your logs.

  • Confirm you see a JSON response containing "UserName": "forensics-test-user" and an ARN.
  • Create an access key for this user by running:
aws iam create-access-key --user-name forensics-test-user

What does this command do?

This generates a programmatic access key pair (an Access Key ID and Secret Access Key) for forensics-test-user. The output contains both values. You need them to configure a CLI profile that impersonates this user.

The Secret Access Key is only shown once at creation time. Copy both values immediately.

📣 Save both keys now

Copy the AccessKeyId and SecretAccessKey from the output. You cannot retrieve the secret key again after this moment. Paste them into a temporary text file or keep the terminal visible.

  • Configure a named CLI profile for the test user by running:
aws configure --profile forensics-test-user

What will this prompt ask?

The aws configure wizard prompts for four values:

  • AWS Access Key ID: paste the Access Key ID you just copied.
  • AWS Secret Access Key: paste the Secret Access Key you just copied.
  • Default region name: enter the same region your trail is in (e.g. us-east-1).
  • Default output format: enter json.
  • Enter each value when prompted, pressing Enter after each one.

Now simulate attacker reconnaissance by running API calls with the test user's credentials. These calls will all be recorded in CloudTrail.

  • Verify the test user profile works by running:
aws sts get-caller-identity --profile forensics-test-user
  • Confirm you see a response showing the test user's ARN and account ID.
  • Run the remaining reconnaissance calls by running these commands:
aws s3 ls --profile forensics-test-user
aws iam list-users --profile forensics-test-user
aws ec2 describe-instances --profile forensics-test-user

Why these specific API calls?

These four commands mimic what a real attacker does after compromising a credential. They check who they are (sts get-caller-identity), enumerate storage (s3 ls), list other users (iam list-users), and look for compute resources (ec2 describe-instances).

Some of these calls may return AccessDenied errors because the test user has no permissions. That is expected. CloudTrail still records the attempt, which is exactly what you want for the investigation.

🙋‍♀️ Seeing AccessDenied on every command?

This is normal. The test user has no IAM policies attached, so most API calls will be denied. CloudTrail records both successful and denied calls. Your forensic queries in Step 4 will show these denied attempts alongside the one successful call (GetCallerIdentity, which requires no permissions).

If sts get-caller-identity also fails, double-check that you pasted the correct Access Key ID and Secret Access Key during aws configure --profile forensics-test-user.

Need help? Why is my forensics-test-user profile not authenticating?

Generate GuardDuty sample findings

Your recon API calls are now in CloudTrail, but GuardDuty needs time to process real events into findings. Instead of waiting hours, you will generate sample findings that simulate what GuardDuty produces during an actual credential compromise.

  • Generate sample findings by running this command (replace your-detector-id with the detector ID you noted in Step 1):
aws guardduty create-sample-findings --detector-id [[DETECTOR_ID="your-detector-id"]] --finding-types UnauthorizedAccess:IAMUser/MaliciousIPCaller CredentialAccess:IAMUser/AnomalousBehavior Discovery:IAMUser/AnomalousBehavior

What do these finding types mean?

Each finding type follows the GuardDuty naming format: ThreatPurpose:ResourceTypeAffected/ThreatFamilyName.

  • UnauthorizedAccess:IAMUser/MaliciousIPCaller means an API call came from a known malicious IP address.
  • CredentialAccess:IAMUser/AnomalousBehavior means credential-stealing behaviour was detected.
  • Discovery:IAMUser/AnomalousBehavior means reconnaissance-style enumeration was detected.

Together, these three findings paint the picture of a compromised credential being used for reconnaissance from a suspicious location.

  • Confirm the command returns with no error output. A successful call produces no output.

Getting a DetectorNotFound error?

Double-check that your detector ID is correct by re-running aws guardduty list-detectors. Make sure you are using the detector ID for the same region your CLI is configured in.

Still stuck? Help me fix my GuardDuty create-sample-findings command.

Confirm findings are visible in the GuardDuty console

Sample findings typically appear within 2-3 minutes. Before you check, think about what you expect to see.

Do you think the sample findings will look different from real detections in the console? What would distinguish them?

  • Wait 2-3 minutes for the findings to populate.
  • Switch to the AWS Management Console and navigate to the GuardDuty service.
  • Click Findings in the left navigation panel.
  • Confirm you see findings prefixed with [SAMPLE] in the findings list.
  • Look for the three finding types you generated: UnauthorizedAccess:IAMUser/MaliciousIPCaller, CredentialAccess:IAMUser/AnomalousBehavior, and Discovery:IAMUser/AnomalousBehavior.

You should see the [SAMPLE] prefix on each finding. This is how GuardDuty distinguishes generated findings from real detections. In a real incident, findings appear without this prefix.

Don't see any findings yet?

Sample findings can take up to 5 minutes to appear. Refresh the Findings page after waiting. Also confirm you are viewing the GuardDuty console in the same region where you ran the create-sample-findings command.

Need help? Why don't I see sample findings in my GuardDuty console?

You now have both layers of evidence for your investigation: raw CloudTrail events from the test user's API calls, and GuardDuty alerts that flag the compromise. In the next step, you will triage these GuardDuty findings and extract the forensic indicators that kick off your deep investigation.

Triage the GuardDuty Findings

Your simulated attack generated both CloudTrail events and GuardDuty alerts. Now it is time to act like a forensic investigator and triage those alerts.

GuardDuty tells you what was detected, but not the full story. In this step you will extract the forensic indicators from the findings and use them as starting points for a deeper investigation.

In this step, get ready to:
  • Examine a GuardDuty finding and extract forensic indicators (Access Key ID, source IP, API action, timestamps).
  • Review severity levels and explore multiple finding types.
  • Identify the investigation gap that GuardDuty alone cannot fill.
Examine the UnauthorizedAccess finding

The first finding to investigate is the credential compromise alert. This finding represents the initial detection that something suspicious happened with an IAM credential.

  • Switch back to the GuardDuty console Findings page where your sample findings are listed.
  • Locate the finding with [SAMPLE] in its title that contains UnauthorizedAccess:IAMUser/MaliciousIPCaller.
  • Click the finding's Title to open the finding details side panel.

The details panel reveals the forensic data embedded in this alert. Look for these key fields:

  • In the Resource affected section, find the Access Key ID and User name. Write these down. The Access Key ID starts with AKIA for long-term credentials.
  • In the Action section, find the API that was called and the Service name. Write these down.
  • In the Actor section, find the IP address and Location fields. Write these down.
  • Note the Updated at timestamp at the top of the panel.

What is the finding type format?

GuardDuty finding types follow a structured naming convention:

ThreatPurpose:ResourceTypeAffected/ThreatFamilyName.DetectionMechanism!Artifact

For UnauthorizedAccess:IAMUser/MaliciousIPCaller, the threat purpose is UnauthorizedAccess, the affected resource type is IAMUser, and the threat family is MaliciousIPCaller. This tells you at a glance that an IAM credential was used from a known malicious IP.

💡 What does AKIA mean?

Access Key IDs starting with AKIA are long-term IAM credentials. Keys starting with ASIA are temporary STS credentials. Knowing which type was compromised changes your remediation strategy.

Review severity levels and examine additional findings

A single finding is just one piece of the puzzle. Examining multiple findings for the same credential reveals the attacker's progression through different tactics.

  • Close the details panel and return to the findings list.
  • Locate the [SAMPLE] CredentialAccess:IAMUser/AnomalousBehavior finding and click its Title to open the details panel.
  • Note the Severity value shown in the panel. All three of your sample findings are classified as Medium severity.
  • Close this panel, then open the [SAMPLE] Discovery:IAMUser/AnomalousBehavior finding the same way.

How do severity levels and tactics connect?

GuardDuty findings map to severity levels: Low (1.0-3.9), Medium (4.0-6.9), High (7.0-8.9), and Critical (9.0-10.0). Your three findings map to different MITRE ATT&CK tactics.

UnauthorizedAccess maps to Initial Access. CredentialAccess maps to Credential Access. Discovery maps to the Discovery tactic. Together, these findings paint a picture of an attacker who gained access with stolen credentials, then enumerated the environment.

Now export the finding JSON to see the full data structure available to automated tooling.

  • With one of the findings selected, click the Actions menu at the top of the findings list.
  • Select View and export JSON.
  • In the Findings JSON window, click Download to save the file to your computer.

The downloaded JSON contains the complete finding data structure including fields not shown in the console view.

Timestamps differ by source

Timestamps in the GuardDuty console appear in your local time zone. JSON exports and CLI outputs display timestamps in UTC. Keep this in mind when correlating with CloudTrail later.

Document your forensic indicators and identify the gap

You now have the starting points for your investigation. Before moving on, consolidate what you have extracted into a clear list of forensic indicators.

  • Open a new file in VS Code or a text editor.
  • Record at least these three indicators from the findings you examined:
  • Add the timestamps (when the activity was first and last seen) for reference.

Now ask yourself: based on what GuardDuty told you, can you determine everything this compromised credential did in your account?

GuardDuty flagged specific suspicious API calls. But your test user also called GetCallerIdentity, ListBuckets, ListUsers, and DescribeInstances. GuardDuty only surfaced alerts for behaviour it classified as anomalous. It cannot tell you the full scope of what the credential did. That is the investigation gap.

Why can't GuardDuty answer the full scope question?

GuardDuty is a detection service, not an audit trail. It generates findings for activity that matches threat intelligence or anomaly models. Routine API calls that happen to be made by a compromised credential may not trigger an alert.

To answer "what else did this credential do?", you need the raw audit log. That is CloudTrail. To query those logs at scale, you need a SQL engine. That is Amazon Athena. The next step connects these pieces.

You have triaged the GuardDuty findings and identified exactly where the detection layer ends. Next up, you will use Amazon Athena to query CloudTrail logs and uncover the full attack chain that GuardDuty could not reveal.

Deep Investigation with CloudTrail and Athena

You have extracted forensic indicators from GuardDuty including the Access Key ID, source IP, and flagged API calls. The critical question remains: what else did the compromised credential do beyond what GuardDuty detected?

Amazon Athena lets you run SQL queries directly against CloudTrail logs stored in S3. This turns millions of raw JSON log events into a searchable forensic database. This is how AWS security teams actually investigate incidents at scale.

In this step, get ready to:
  • Create an Athena database and external table over your CloudTrail logs using partition projection.
  • Run forensic SQL queries that reveal every action the compromised credential took.
  • Separate successful actions from denied ones and identify all source IPs used by the attacker.
Open the Athena console and create your forensics database

Before you can query CloudTrail logs, you need an Athena database to hold your table definitions and a query results location where Athena stores output.

  • In the AWS Management Console, search for Athena and open the Athena service.
  • If this is your first time visiting Athena, click Explore the query editor to open the query editor.
  • Click Edit Settings in the banner at the top of the query editor.
  • In the Location of query result box, enter the path to your forensics bucket prefixed with s3:// (for example, s3://your-forensics-bucket/athena-results/).
  • Click Save.
  • Click Editor to return to the query editor.

Now create a database to hold your forensic tables.

  • Paste the following SQL into the Athena query editor and click Run:
CREATE DATABASE IF NOT EXISTS forensics_db;
  • From the Database dropdown on the left panel, select forensics_db to make it your current database.

You should see the message "Query successful" in the results panel and forensics_db now appears in the database dropdown.

Seeing an error about query result location?

Athena requires a query result location before it can run any query. Make sure you completed the Edit Settings step above and entered a valid S3 path starting with s3://.

help me fix my Athena query result location error

Create the CloudTrail external table with partition projection

Partition projection automatically maps new daily log partitions as CloudTrail delivers them. This eliminates the need to manually add partitions and lets you focus on writing forensic queries.

  • Clear the query editor, then paste the first part of the CREATE TABLE statement. This defines the table name and the complex useridentity struct that holds the actor details for each event:
CREATE EXTERNAL TABLE cloudtrail_logs_pp(
    eventversion STRING,
    useridentity STRUCT<
        type: STRING,
        principalid: STRING,
        arn: STRING,
        accountid: STRING,
        invokedby: STRING,
        accesskeyid: STRING,
        username: STRING,
        sessioncontext: STRUCT<
            attributes: STRUCT<
                mfaauthenticated: STRING,
                creationdate: STRING>,
            sessionissuer: STRUCT<
                type: STRING,
                principalid: STRING,
                arn: STRING,
                accountid: STRING,
                username: STRING>
        >
    >,
    eventtime STRING,
    eventsource STRING,
    eventname STRING,
    awsregion STRING,
    sourceipaddress STRING,
    useragent STRING,
    errorcode STRING,
    errormessage STRING,

What does the useridentity struct capture?

The useridentity struct is the most forensically important field. It tells you WHO performed each action: the IAM type, the ARN, the access key ID, and session details.

The accesskeyid field inside this struct is what you will filter on to trace all activity by the compromised credential.

  • Continue typing (or paste) the remaining column definitions directly below the previous code in the same query editor. These capture the event metadata, resources accessed, and TLS connection details:
    requestparameters STRING,
    responseelements STRING,
    additionaleventdata STRING,
    requestid STRING,
    eventid STRING,
    readonly STRING,
    resources ARRAY<STRUCT<
        arn: STRING,
        accountid: STRING,
        type: STRING>>,
    eventtype STRING,
    apiversion STRING,
    recipientaccountid STRING,
    serviceeventdetails STRING,
    sharedeventid STRING,
    vpcendpointid STRING,
    vpcendpointaccountid STRING,
    eventcategory STRING,
    tlsdetails STRUCT<
        tlsversion: STRING,
        ciphersuite: STRING,
        clientprovidedhostheader: STRING>
)

What do these columns represent?

Each column maps to a field in the CloudTrail JSON log. Key forensic fields include errorcode (tells you if an action was denied), sourceipaddress (attacker location), and eventtime (timeline reconstruction).

  • Finally, add the partitioning, storage format, and location configuration directly below the closing parenthesis. Replace the placeholder values with your actual bucket name, account ID, and region:
PARTITIONED BY (
   `timestamp` string)
ROW FORMAT SERDE 'org.apache.hive.hcatalog.data.JsonSerDe'
STORED AS INPUTFORMAT 'com.amazon.emr.cloudtrail.CloudTrailInputFormat'
OUTPUTFORMAT 'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat'
LOCATION
  's3://[[FORENSICS_BUCKET="your-forensics-bucket"]]/AWSLogs/[[AWS_ACCOUNT_ID="your-account-id"]]/CloudTrail/[[AWS_REGION="your-region"]]'
TBLPROPERTIES (
  'projection.enabled'='true',
  'projection.timestamp.format'='yyyy/MM/dd',
  'projection.timestamp.interval'='1',
  'projection.timestamp.interval.unit'='DAYS',
  'projection.timestamp.range'='2024/01/01,NOW',
  'projection.timestamp.type'='date',
  'storage.location.template'='s3://[[FORENSICS_BUCKET="your-forensics-bucket"]]/AWSLogs/[[AWS_ACCOUNT_ID="your-account-id"]]/CloudTrail/[[AWS_REGION="your-region"]]/${timestamp}');

What does partition projection do?

CloudTrail stores logs in daily folders like 2026/08/06/. Partition projection tells Athena to automatically map these date-based folders to query partitions.

The storage.location.template uses ${timestamp} to match the folder structure. This means you can query any date range without manually registering partitions.

💡 Find your account ID and region

Your AWS account ID appears in the top-right corner of the console when you click your username. Your region is shown in the region dropdown next to it (for example, us-east-1). The LOCATION path must match the exact folder structure CloudTrail created in your S3 bucket.

  • Confirm the full statement is assembled in the query editor (all three sections combined as one continuous SQL statement), then click Run.

You should see "Query successful" in the results panel. In the left panel under Tables, cloudtrail_logs_pp now appears under your forensics_db database.

Table not appearing or query failed?

Check that the LOCATION path matches your actual S3 folder structure. Open your forensics bucket in the S3 console and verify the path follows the pattern AWSLogs/your-account-id/CloudTrail/your-region/.

Also confirm you selected forensics_db in the Database dropdown before running the CREATE TABLE statement.

help me fix my Athena CloudTrail table creation error

Run your first forensic query to trace the compromised credential

Your table is ready. Now you will query it to answer the question GuardDuty could not: what is the full scope of everything the compromised access key did?

  • Clear the query editor and paste this forensic query. Replace the access key ID with the one you documented from the GuardDuty findings, and adjust the date to today's date in yyyy/MM/dd format:
-- Query 1: All activity by the compromised access key
SELECT
    eventtime,
    eventname,
    eventsource,
    sourceipaddress,
    useragent,
    errorcode,
    useridentity.arn AS user_arn,
    useridentity.accesskeyid AS access_key
FROM cloudtrail_logs_pp
WHERE useridentity.accesskeyid = '[[ACCESS_KEY_ID="your-access-key-id"]]'
    AND timestamp BETWEEN '[[START_DATE="2026/08/06"]]' AND '[[END_DATE="2026/08/06"]]'
ORDER BY eventtime ASC;

What does this query reveal?

This query filters the entire CloudTrail log history for a single access key, ordered chronologically. GuardDuty only flagged the calls it deemed suspicious. This query shows EVERY API call the credential made, including ones GuardDuty did not alert on.

The ORDER BY eventtime ASC clause reconstructs the timeline in the order the attacker acted.

  • Click Run to execute the query.

You should see results showing the reconnaissance API calls your test user made: GetCallerIdentity, ListBuckets, ListUsers, and DescribeInstances. Each row includes the timestamp, source IP address, and the service that was called.

Why is this more powerful than GuardDuty alone?

GuardDuty detected suspicious patterns, but your Athena query just revealed the full picture. You can now see every service the credential touched, the exact sequence of actions, and the IP addresses involved. This is the difference between an alert and an investigation.

Separate successful and denied actions, then trace source IPs

Knowing what the attacker attempted is important, but knowing what they actually achieved is critical. The errorcode field distinguishes successful API calls from ones IAM policies blocked.

  • Clear the query editor and paste this query to classify each action as successful or denied:
-- Query 2: Separate successful vs denied actions
SELECT
    eventtime,
    eventname,
    eventsource,
    sourceipaddress,
    CASE
        WHEN errorcode IS NULL THEN 'SUCCESS'
        ELSE errorcode
    END AS result
FROM cloudtrail_logs_pp
WHERE useridentity.accesskeyid = '[[ACCESS_KEY_ID="your-access-key-id"]]'
    AND timestamp BETWEEN '[[START_DATE="2026/08/06"]]' AND '[[END_DATE="2026/08/06"]]'
ORDER BY eventtime ASC;

What does the CASE statement do?

When an API call succeeds, CloudTrail leaves errorcode as NULL. When IAM denies the call, the field contains the error like AccessDenied. The CASE statement converts this into a readable SUCCESS or the specific error name, making it easy to spot which actions the attacker pulled off versus which were blocked.

  • Click Run.

You should see each API call now labeled with either SUCCESS or an error code. This tells you exactly what the attacker achieved versus what your IAM policies prevented.

  • Now run one final query to identify all source IP addresses that used this credential:
-- Query 3: Identify unique source IPs used by the credential
SELECT
    sourceipaddress,
    COUNT(*) AS event_count,
    MIN(eventtime) AS first_seen,
    MAX(eventtime) AS last_seen
FROM cloudtrail_logs_pp
WHERE useridentity.accesskeyid = '[[ACCESS_KEY_ID="your-access-key-id"]]'
    AND timestamp BETWEEN '[[START_DATE="2026/08/06"]]' AND '[[END_DATE="2026/08/06"]]'
GROUP BY sourceipaddress
ORDER BY event_count DESC;

Why trace source IPs?

In a real compromise, an attacker's IP address differs from the legitimate user's IP. Multiple IPs using the same credential is a strong indicator that the key was stolen and used from a different location.

The first_seen and last_seen columns define the attacker's activity window, which feeds directly into your forensic report.

  • Click Run.

Before you check the results, predict: how many unique IP addresses do you expect to see for your test user's credential?

You should see at least one IP address (your current IP) with the count of API calls made and the time window. In a real incident, multiple IPs here would confirm the credential was used from an unauthorized location.

Queries returning zero results?

CloudTrail logs can take 5-15 minutes to be delivered to S3 after the API calls were made. If you see no results, wait a few more minutes and re-run the query.

Also verify the timestamp BETWEEN values match today's date in yyyy/MM/dd format (for example, 2026/08/06). The partition date must match the folder structure in S3.

help me troubleshoot why my Athena CloudTrail query returns empty results

You have just done what GuardDuty alone could not. You traced the full attack chain from a single alert to a complete timeline of every action, every IP, and every success or failure. Next up, you will validate that this evidence has not been tampered with and compile your findings into a formal forensic report.

Validate Evidence and Compile Forensic Report

Your Athena queries have revealed the full attack chain. You know exactly which API calls the compromised credential made, when, and from where.

In real forensic investigations, uncovering the timeline is only half the job. You also need to prove that your evidence has not been tampered with. CloudTrail's log file integrity validation uses SHA-256 hashing and SHA-256 with RSA signing to cryptographically guarantee that no log file has been modified since delivery. This step completes the NIST forensic cycle: Collection (Step 1), Examination (Steps 2-3), Analysis (Step 4), and now Reporting.

In this step, get ready to:
  • Validate CloudTrail log integrity using the CLI.
  • Compile a structured forensic report following the NIST four-phase methodology.
  • Clean up the test IAM user and its access key.
Validate CloudTrail log integrity

In any legal or compliance context, evidence must be provably untampered. CloudTrail delivers a digest file every hour that contains the SHA-256 hash of each log file. The digest itself is signed with an RSA key. The validate-logs command walks the entire chain and confirms every file is intact.

  • Run the following command in your terminal to validate your trail's log integrity, replacing the trail ARN and start time with your own values:
aws cloudtrail validate-logs \
  --trail-arn [[TRAIL_ARN="arn:aws:cloudtrail:your-region:your-account-id:trail/forensics-trail"]] \
  --start-time [[START_TIME="2026-08-06T00:00:00Z"]]

What does this command do?

The --trail-arn parameter tells AWS which trail's logs to validate. The --start-time sets how far back to check. The command walks the digest chain from the present back to that start time, verifying the RSA signature on each digest and the SHA-256 hash on each referenced log file.

Before you look at the output, what do you expect to see? Do you think all files will be valid, or might some be missing?

You should see output like this:

Validating log files for trail arn:aws:cloudtrail:us-east-1:123456789012:trail/forensics-trail between 2026-08-06T00:00:00Z and 2026-08-06T22:15:43Z
Results requested for 2026-08-06T00:00:00Z to 2026-08-06T22:15:43Z
Results found for 2026-08-06T00:24:57Z to 2026-08-06T21:24:57Z:
3/3 digest files valid
15/15 log files valid

What does this output mean?

The summary line shows X/X digest files valid and Y/Y log files valid. This confirms that every digest file's RSA signature is intact and every referenced log file's SHA-256 hash matches its original value. No one has modified, deleted, or forged any of these log files since CloudTrail delivered them.

Your actual numbers will depend on how long your trail has been running. The important thing is that zero files show as INVALID.

🙋‍♀️ Seeing 0 digest files or 0 log files?

CloudTrail delivers its first digest file approximately one hour after you start logging. If you created the trail less than an hour ago, wait a bit longer and re-run the command.

Also confirm your --start-time is not set to a time before the trail was created. Use the timestamp from when you ran aws cloudtrail start-logging in Step 1.

Still stuck? Help me troubleshoot why my validate-logs command returns 0 files

Compile your forensic report

You now have all four phases of the NIST forensic methodology complete. It is time to assemble everything into a structured forensic report that a security team or auditor could act on.

  • Open VS Code and create a new file called forensic_report.md in your project folder.
  • Copy the following report template into the file, filling in the bracketed sections with the actual values you collected during this investigation:
# Forensic Investigation Report

## 1. Summary of Finding (What GuardDuty Detected)

- Finding type: UnauthorizedAccess:IAMUser/MaliciousIPCaller
- Severity: Medium
- Compromised resource: IAM User forensics-test-user
- Access Key ID: [[ACCESS_KEY_ID="your test user's access key ID"]]
- Source IP flagged: [[SOURCE_IP="the IP address from the GuardDuty finding"]]

## 2. Scope of Compromise (What Athena Revealed)

- Total API calls by compromised credential: [[TOTAL_CALLS="number from your Query 1 results"]]
- Services accessed: sts, s3, iam, ec2
- Successful actions: GetCallerIdentity, ListBuckets, ListUsers, DescribeInstances
- Denied actions: [[DENIED="any denied actions from Query 2, or 'None'"]]

## 3. Timeline of Events

| Time (UTC) | API Action | Service | Result |
|---|---|---|---|
| [[TIME1="first eventtime"]] | GetCallerIdentity | sts.amazonaws.com | SUCCESS |
| [[TIME2="second eventtime"]] | ListBuckets | s3.amazonaws.com | SUCCESS |
| [[TIME3="third eventtime"]] | ListUsers | iam.amazonaws.com | SUCCESS |
| [[TIME4="fourth eventtime"]] | DescribeInstances | ec2.amazonaws.com | SUCCESS |

## 4. Evidence Integrity

- Validation command: aws cloudtrail validate-logs
- Result: All digest files valid, all log files valid
- Method: SHA-256 hashing with RSA digital signing
- Conclusion: Evidence has not been tampered with since delivery

## 5. Recommended Remediation

- Immediately disable the compromised access key
- Delete the compromised access key
- Review IAM permissions for the affected user
- Investigate the source IP for additional unauthorized activity
- Enable MFA on all IAM users with programmatic access

What does each section cover?

  • Section 1 maps to what GuardDuty initially detected. This is your starting point.
  • Section 2 maps to what Athena revealed beyond the initial alert. This is the full scope.
  • Section 3 is the ordered timeline you reconstructed from Query 1, sorted by eventtime.
  • Section 4 records your evidence integrity validation from the previous substep.
  • Section 5 lists the concrete remediation actions an incident responder would take.
  • Save forensic_report.md.

You now have a complete, structured forensic report that traces an attack from initial detection through full timeline reconstruction, backed by cryptographically validated evidence.

Clean up the test IAM user

In a real incident, the first remediation action is to disable and delete the compromised credential. You will now do exactly that for your test user, which also demonstrates the remediation step of a forensic workflow.

  • Delete the test user's access key by running this command, replacing the access key ID with the one you documented earlier:
aws iam delete-access-key \
  --user-name forensics-test-user \
  --access-key-id [[TEST_ACCESS_KEY_ID="your test user's access key ID"]]

This command produces no output on success.

  • Now delete the IAM user itself by running:
aws iam delete-user --user-name forensics-test-user

This command also produces no output on success.

  • Confirm the user no longer exists by running:
aws iam get-user --user-name forensics-test-user

Before you run this, what do you expect the output to be? The user was just deleted.

You should see an error message indicating that the user cannot be found. This confirms the compromised credential has been fully removed from your account.

Why delete the user last?

AWS requires you to delete all resources attached to a user before you can delete the user itself. That means access keys must go first, then the user. This mirrors real incident response where you disable the key immediately (cutting off the attacker) and clean up the user entity afterwards.

🙋‍♀️ Getting a DeleteConflict error?

If you see a DeleteConflict when trying to delete the user, the user still has attached resources. Run aws iam list-access-keys --user-name forensics-test-user to check for remaining keys. If any policies are attached, detach them first with aws iam detach-user-policy.

Need more help? Help me delete the forensics-test-user IAM user

Your forensic investigation is complete. You started from a GuardDuty alert, traced the full attack chain through CloudTrail with Athena, proved your evidence is untampered, and compiled a report that follows the NIST four-phase methodology. The forensics trail remains active and continues to collect evidence for future investigations.

Secret mission

Export Forensic Evidence to a Persistent Database

Your Athena queries answered the investigation questions, but those results disappear after each session. What if you could save them as a permanent, browsable table that your team can access any time? Use Athena's CTAS to export your forensic evidence into a persistent Parquet table registered in the AWS Glue Data Catalog.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project uses cloud resources with minimal ongoing costs from S3 storage and CloudTrail logging.

Cost warning

Leaving the forensics trail active delivers logs continuously to S3. For a low-activity account, expect less than $5/month in combined storage and CloudTrail charges.

If you completed the Secret Mission, the GuardDuty export bucket also accumulates findings data. To stop all charges, choose the Delete option below.

Resources you used:

  • CloudTrail trail (forensics-trail): delivers logs to S3. The first management trail is free. Additional trails cost $2.00 per 100,000 management events.
  • S3 bucket for CloudTrail logs (your-forensics-bucket): stores log files at ~$0.023/GB/month.
  • S3 bucket for GuardDuty export (your-guardduty-export-bucket): stores exported GuardDuty findings JSON.
  • Athena tables (cloudtrail_logs_pp and guardduty_findings): metadata only, no ongoing cost unless you run queries ($5 per TB scanned).
  • GuardDuty findings export configuration: a setting within GuardDuty that writes findings to S3.

Keep everything running

No action needed. Choose this if you want the forensics trail to keep collecting evidence for ongoing monitoring.

  • The forensics trail continues delivering logs to your S3 bucket.
  • GuardDuty findings continue exporting to your export bucket.
  • Athena tables remain available for future queries.
  • Estimated ongoing cost for a low-activity account: less than $5/month.

Pause - I'll come back to this later

Shut down active logging to stop new charges while keeping your trail configuration and data intact.

  • Stop the CloudTrail trail from logging by running:
aws cloudtrail stop-logging --name forensics-trail
  • Your trail configuration persists, but no new logs or charges accumulate.
  • Existing log files in S3 remain with a small ongoing storage cost.
  • To resume later, run aws cloudtrail start-logging --name forensics-trail.

Delete - I don't want to use this again

Remove all project resources and start fresh. This permanently deletes your forensic logs and trail configuration.

  • Delete the CloudTrail trail by running:
aws cloudtrail delete-trail --name forensics-trail
  • Empty and delete the CloudTrail logs bucket by running:
aws s3 rb s3://[[FORENSICS_BUCKET="your-forensics-bucket"]] --force
  • Empty and delete the GuardDuty export bucket by running:
aws s3 rb s3://[[GUARDDUTY_BUCKET="your-guardduty-export-bucket"]] --force
  • Drop the Athena tables by running these queries in the Athena query editor:
DROP TABLE IF EXISTS cloudtrail_logs_pp;
DROP TABLE IF EXISTS guardduty_findings;
  • Disable the GuardDuty findings export by navigating to the GuardDuty console, selecting Settings, scrolling to Findings export options, and clicking Edit to remove the S3 bucket configuration.
  • GuardDuty itself remains active independently. It was enabled before this project and does not need to be disabled.

What about the data in S3?

Dropping an Athena table only removes the table metadata from the AWS Glue Data Catalog. It does not delete the underlying data in S3.

The aws s3 rb --force commands above handle deleting the actual log files and buckets.

Nice Work!

Nice Work!

Nice work! You just built a complete digital forensics investigation workflow in AWS, tracing a simulated IAM credential compromise from initial detection through full timeline reconstruction with cryptographically validated evidence.

You've learned how to:

  • Triage GuardDuty findings to extract forensic indicators (Access Key ID, source IP, API calls) and identify the limitations of detection-only tools as investigation starting points.
  • Build an Athena table over CloudTrail logs using partition projection and write SQL queries that trace a full attack chain beyond what GuardDuty alone could reveal.
  • Validate log evidence integrity using CloudTrail's SHA-256 hashing and RSA signing, then compile a structured forensic report following the NIST four-phase methodology (Collection, Examination, Analysis, Reporting).
  • Secret Mission: Exported forensic evidence to a persistent Parquet table using Athena's CTAS and verified it in the AWS Glue Data Catalog.

Ready to quiz yourself?