---
cuppy_hub: aws
cuppy_path: guides/cloudtrail/find-who-did-what.md
cuppy_section: docs
cuppy_doc: 143C-87EV
cuppy_doc_uuid: b24c52be-12a0-4c16-9e2e-9174cf929275
cuppy_permalink: https://cuppy.io/aws/d/143C-87EV
cuppy_version: 4
cuppy_updated: 2026-10-23T16:16:52.575Z
cuppy_canonical: https://cuppy.io/aws/docs/guides/cloudtrail/find-who-did-what
cuppy_mcp: https://cuppy.io/mcp
cuppy_body_sha256: d71b1dc4f80303d4bd5e424dfc8b1dfea5bdb41381e100f1fc395b89fc02a093
# cuppy_body_sha256 is over the document body ALONE, excluding both generated blocks.
---


# Find who changed something, or why a call was denied

**For a control-plane change in the last 90 days, ask Event history first: it is on in every account, free, and needs no setup.** Search by `ResourceName` or `EventName` in the Region where the call was made. Object-level S3 reads and writes, Lambda invokes and other **data events** are *not* there. They exist only if a trail or event data store was configured to record them beforehand.

## Pick the tool

| Question | Use | Limits |
|---|---|---|
| Who changed or deleted resource X (last 90 days)? | **Event history** / `lookup-events` | Management events only, one Region per call, one filter per call, 2 requests/s per account per Region, 50 events per page |
| Every denied call in the last hour, by principal | A **trail** delivering to CloudWatch Logs, then **Logs Insights** | Needs the trail set up before the event happened |
| Across accounts or beyond 90 days, with SQL | **CloudTrail Lake** (event data store) | Paid per data scanned; retention set by the pricing option |
| S3 object access, Lambda invocations | A trail or event data store with **data events** enabled | Opt-in and billed. Advanced selectors narrow it |

Events reach trails about **5 minutes** after the call on average, and that isn't guaranteed. If a change from a minute ago is missing, wait.

## 1. Event history: who touched this resource?

```
aws cloudtrail lookup-events --region <region> \
  --lookup-attributes AttributeKey=ResourceName,AttributeValue=<name-or-id> \
  --start-time 2026-10-17T00:00:00Z --max-items 50
```

- `AttributeKey` is case-sensitive and takes one of: `AccessKeyId`, `EventId`, `EventName`, `EventSource`, `ReadOnly`, `ResourceName`, `ResourceType`, `Username`. You get **one** pair per call, so filter the rest yourself.
- `AttributeKey=ReadOnly,AttributeValue=false` returns only writes.
- Results come newest first. `CloudTrailEvent` is the whole record as a JSON **string**: parse it to read `userIdentity`, `errorCode` and `requestParameters`.
- Global-service calls such as IAM are recorded in a specific Region. Repeat the lookup in `us-east-1` if your Region shows nothing (UNVERIFIED: the Region rule for global services isn't on the fetched pages).

```ts
import { CloudTrailClient, paginateLookupEvents } from "@aws-sdk/client-cloudtrail";

const ct = new CloudTrailClient({ region: "us-east-1" });

/** Writes to a resource in the last `hours`, with who made them and whether they failed. */
export async function whoTouched(resourceName: string, hours = 24) {
  const out: { time?: Date; event?: string; who?: string; error?: string }[] = [];
  const pages = paginateLookupEvents({ client: ct, pageSize: 50 }, {
    LookupAttributes: [{ AttributeKey: "ResourceName", AttributeValue: resourceName }],
    StartTime: new Date(Date.now() - hours * 3600_000),
  });
  for await (const page of pages) {
    for (const e of page.Events ?? []) {
      const rec = JSON.parse(e.CloudTrailEvent ?? "{}");
      if (rec.readOnly) continue; // the API allows one filter only, so filter writes here
      out.push({ time: e.EventTime, event: e.EventName, who: rec.userIdentity?.arn, error: rec.errorCode });
    }
  }
  return out;
}

console.table(await whoTouched("my-bucket"));
```

Event history allows only 2 requests per second. The SDK retries `ThrottlingException`, but for wide searches use a trail or Lake.

## 2. Find denied calls (trail → CloudWatch Logs → Logs Insights)

A trail can send its events to a log group, and a multi-Region trail sends every Region to that one group. Then query it:

```
filter ispresent(errorCode)
| stats count(*) as calls by errorCode, eventSource, eventName, userIdentity.arn
| sort calls desc
| limit 50
```

Narrow it to authorization failures. Services spell these differently, for example `AccessDenied`, `AccessDeniedException` or EC2's `UnauthorizedOperation`, sometimes with a prefix. The regex matches on substrings, so it catches the variants:

```
filter errorCode like /AccessDenied|UnauthorizedOperation/
| fields @timestamp, userIdentity.arn, eventSource, eventName, errorMessage
| sort @timestamp desc
| limit 100
```

- `errorMessage` carries the authorization failure text. Decode it with `guides/iam/fix-access-denied.md`.
- Denials by a **VPC endpoint policy** show `errorCode` `VpceAccessDenied` and the message "The request was denied due to a VPC endpoint policy." (`patterns/iam-to-vpc-endpoints.md`).
- More queries are in `reference/cloudwatch/logs-insights-queries.md`.

## 3. Read the identity

Most calls come from roles. In an `AssumedRole` record:

- `userIdentity.arn` = `arn:aws:sts::<account>:assumed-role/<RoleName>/<SessionName>`. The **session name** is your best clue to the human or job. CI sessions named after a run ID, such as `gha-<run id>` from `guides/iam/ci-oidc-github.md`, map straight to a workflow run.
- `userIdentity.sessionContext.sessionIssuer.arn` is the role itself.
- `userIdentity.sessionContext.sourceIdentity` is the original user, if your STS setup requires a source identity. It follows the session through role chains.
- `invokedBy` names the AWS service that made the call on your behalf. Examples are CloudFormation with a service role, or Auto Scaling.
- `onBehalfOf.userId` is the Identity Center user for `IdentityCenterUser` events.
- Cross-account calls set `recipientAccountId` to the account that received the call, and `sharedEventID` links the copies in both accounts.

To get from a session back to whoever started it, find the `AssumeRole`, `AssumeRoleWithWebIdentity` or `AssumeRoleWithSAML` event that created it: same session name, around `attributes.creationDate`. Field-by-field detail is in `reference/cloudtrail/event-records.md`.

## Nothing found?

- **Wrong Region:** Event history and trails are Regional unless the trail is multi-Region.
- **Data event:** not recorded unless it was enabled beforehand.
- **Too recent:** delivery averages about 5 minutes.
- **Older than 90 days:** only a trail's S3 copy or Lake has it.

CloudTrail API errors (`LookupEvents` throttling, trail delivery, Lake queries): `reference/cloudtrail/errors.md`.


---
<!-- cuppy -->
You are reading one file. The same content is free over MCP at https://cuppy.io/mcp with no signup: ranked search across all 131 documents in this hub, the merged view when you work in a layer, known workarounds matched to the error you hit, and depth control so you spend tokens on what you need. Start with the `_hello` tool. How to connect: https://cuppy.io/aws/ Something wrong in this document? Report it with the `session feedback` tool on the same endpoint.
<!-- /cuppy -->
