Skip to content

Commit 1017304

Browse files
waleedlatif1claude
andcommitted
fix(cloudtrail): address cubic review findings
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LJV2dCcAYvf1JuxquCMZAd
1 parent 5f455d1 commit 1017304

14 files changed

Lines changed: 357 additions & 118 deletions

‎apps/docs/content/docs/integrations/cloudtrail.mdx‎

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
1111
/>
1212

1313
{/* MANUAL-CONTENT-START:intro */}
14-
[AWS CloudTrail](https://aws.amazon.com/cloudtrail/) records who did what in your AWS accounts. Every API call — from the console, the CLI, an SDK, or another AWS service — is captured as an event with the calling identity, source IP, parameters, and result. It is the system of record for security investigation, compliance evidence, and answering "what changed?"
14+
[AWS CloudTrail](https://aws.amazon.com/cloudtrail/) records who did what in your AWS accounts. An API call — from the console, the CLI, an SDK, or another AWS service — is captured as an event with the calling identity, source IP, parameters, and result. It is the system of record for security investigation, compliance evidence, and answering "what changed?"
15+
16+
What lands in that record is set by configuration, not assumed. Trails and event data stores log management events by default; data events, network activity events, and Insights events are captured only where you configure selectors for them. Read a trail's selectors before you treat its history as complete.
1517

1618
With AWS CloudTrail, you can:
1719

@@ -22,9 +24,39 @@ With AWS CloudTrail, you can:
2224

2325
In Sim, CloudTrail is the audit half of the AWS story. Where IAM and Identity Center answer *who has access*, CloudTrail answers *what they actually did with it* — so an agent can take a suspicious permission change and trace it back to the principal, the source IP, and the moment it happened, then hand a written timeline to whoever needs to act on it.
2426

25-
This block is read-only with one narrow exception: `Cancel Query` stops a running CloudTrail Lake query. It never enables or disables logging, alters trail configuration, or deletes a trail, so it can be granted a `cloudtrail:Describe*/Get*/List*/LookupEvents/StartQuery` policy with no residual write risk. Note that `Start Query` is billed per GB scanned and consumes your account's concurrent-query quota.
26-
27-
`Lookup Events` is limited by AWS to two requests per second per account per Region. The block paces itself against that limit automatically, so broad searches take longer rather than failing.
27+
This block is read-only with one narrow exception: `Cancel Query` stops a running CloudTrail Lake query. It never enables or disables logging, alters trail configuration, or deletes a trail. Every operation it ships is covered by this policy, with no residual write risk:
28+
29+
```json
30+
{
31+
"Version": "2012-10-17",
32+
"Statement": [
33+
{
34+
"Effect": "Allow",
35+
"Action": [
36+
"cloudtrail:DescribeTrails",
37+
"cloudtrail:GetTrail",
38+
"cloudtrail:GetTrailStatus",
39+
"cloudtrail:GetEventSelectors",
40+
"cloudtrail:GetInsightSelectors",
41+
"cloudtrail:GetEventDataStore",
42+
"cloudtrail:ListTrails",
43+
"cloudtrail:ListEventDataStores",
44+
"cloudtrail:ListTags",
45+
"cloudtrail:LookupEvents",
46+
"cloudtrail:StartQuery",
47+
"cloudtrail:DescribeQuery",
48+
"cloudtrail:GetQueryResults",
49+
"cloudtrail:CancelQuery"
50+
],
51+
"Resource": "*"
52+
}
53+
]
54+
}
55+
```
56+
57+
`cloudtrail:CancelQuery` is the action `Cancel Query` needs, and it is not implied by `Describe*`, `Get*`, or `List*` — omit it and that one operation fails with an access-denied error. Note that `Start Query` is billed per GB scanned and consumes your account's concurrent-query quota of 10.
58+
59+
`Lookup Events` is limited by AWS to two requests per second per account per Region. Each call uses AWS adaptive retry mode and allows up to six attempts, so a throttled request backs off exponentially with jitter and usually succeeds instead of surfacing an error. That is a retry budget, not a guarantee: sustained throttling past six attempts fails the call with a `ThrottlingException`, and because a fresh SDK client is built per invocation, adaptive mode's client-side rate limiter carries no pacing state between calls. `Lookup Events` also returns one page per call — feed `nextToken` back in to walk a broad search, and expect to handle a throttling error on a long paging loop.
2860
{/* MANUAL-CONTENT-END */}
2961

3062

@@ -192,7 +224,7 @@ List the ARN, name, and home Region of every CloudTrail trail visible to the acc
192224
| ↳ `trailArn` | string | ARN of the trail |
193225
| ↳ `name` | string | Trail name |
194226
| ↳ `homeRegion` | string | Region in which the trail was created |
195-
| `nextToken` | string | Pagination token for the next page of trails |
227+
| `nextToken` | string | Pagination token for the next page of trails, or null on the last page |
196228

197229
### CloudTrail Get Event Selectors
198230

‎apps/sim/lib/api/contracts/tools/aws/cloudtrail-describe-query.ts‎

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ import type {
77
import { defineRouteContract } from '@/lib/api/contracts/types'
88
import { validateAwsRegion } from '@/lib/core/security/input-validation'
99

10+
/**
11+
* `DescribeQuery` requires either `QueryId` or `QueryAlias`. AWS documents `RefreshId` as
12+
* something you "provide along with `QueryAlias`" to read a dashboard refresh, so it is
13+
* rejected when the query is addressed by ID.
14+
* @see https://docs.aws.amazon.com/awscloudtrail/latest/APIReference/API_DescribeQuery.html
15+
*/
1016
const DescribeQuerySchema = z
1117
.object({
1218
region: z
@@ -44,9 +50,21 @@ const DescribeQuerySchema = z
4450
.regex(/^\d+$/, 'Account ID must be numeric')
4551
.optional(),
4652
})
47-
.refine((v) => Boolean(v.queryId) !== Boolean(v.queryAlias), {
48-
message: 'Specify exactly one of queryId or queryAlias',
49-
path: ['queryId'],
53+
.superRefine((v, ctx) => {
54+
if (Boolean(v.queryId) === Boolean(v.queryAlias)) {
55+
ctx.addIssue({
56+
code: 'custom',
57+
message: 'Specify exactly one of queryId or queryAlias',
58+
path: ['queryId'],
59+
})
60+
}
61+
if (v.refreshId && !v.queryAlias) {
62+
ctx.addIssue({
63+
code: 'custom',
64+
message: 'refreshId identifies a dashboard refresh and is only valid with queryAlias',
65+
path: ['refreshId'],
66+
})
67+
}
5068
})
5169

5270
const DescribeQueryResponseSchema = z.object({

‎apps/sim/lib/api/contracts/tools/aws/cloudtrail-describe-trails.ts‎

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { z } from 'zod'
2+
import { cloudtrailTrailNameOrArnSchema } from '@/lib/api/contracts/tools/aws/cloudtrail-shared'
23
import type {
34
ContractBody,
45
ContractBodyInput,
@@ -7,22 +8,6 @@ import type {
78
import { defineRouteContract } from '@/lib/api/contracts/types'
89
import { validateAwsRegion } from '@/lib/core/security/input-validation'
910

10-
/**
11-
* A trail name (3-128 chars, ASCII alphanumerics plus non-adjacent `.`, `_`, `-`,
12-
* starting and ending alphanumeric) or a full trail ARN. Shadow trails and
13-
* organization trails in another Region can only be addressed by ARN.
14-
* @see https://docs.aws.amazon.com/awscloudtrail/latest/APIReference/API_GetTrail.html
15-
*/
16-
const trailNameOrArnSchema = z
17-
.string()
18-
.trim()
19-
.min(3, 'Trail name must be at least 3 characters')
20-
.max(256, 'Trail name or ARN is too long')
21-
.regex(
22-
/^(?:arn:aws[a-zA-Z0-9-]*:cloudtrail:[a-z0-9-]+:\d{12}:trail\/[\w.\-/]+|[a-zA-Z0-9](?:[._-]?[a-zA-Z0-9]+)+)$/,
23-
'Must be a valid trail name or trail ARN'
24-
)
25-
2611
const trailSchema = z.object({
2712
name: z.string(),
2813
s3BucketName: z.string().nullable(),
@@ -51,9 +36,17 @@ const DescribeTrailsSchema = z.object({
5136
}),
5237
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
5338
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
39+
/**
40+
* `DescribeTrails` documents no array-member limit on `trailNameList`, so this ceiling is a
41+
* request-payload guard rather than an AWS constraint. It is deliberately set above anything
42+
* reachable: the trails-per-Region quota is 5, so naming every trail and shadow trail in an
43+
* account across all commercial Regions still stays well under 200.
44+
* @see https://docs.aws.amazon.com/awscloudtrail/latest/APIReference/API_DescribeTrails.html
45+
* @see https://docs.aws.amazon.com/awscloudtrail/latest/userguide/WhatIsCloudTrail-Limits.html
46+
*/
5447
trailNameList: z
55-
.array(trailNameOrArnSchema)
56-
.max(200, 'A maximum of 200 trail names or ARNs can be described at once')
48+
.array(cloudtrailTrailNameOrArnSchema)
49+
.max(200, 'At most 200 trail names or ARNs can be described in one request')
5750
.optional(),
5851
includeShadowTrails: z.boolean().optional(),
5952
})

‎apps/sim/lib/api/contracts/tools/aws/cloudtrail-get-event-selectors.ts‎

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { z } from 'zod'
2+
import { cloudtrailTrailNameOrArnSchema } from '@/lib/api/contracts/tools/aws/cloudtrail-shared'
23
import type {
34
ContractBody,
45
ContractBodyInput,
@@ -7,22 +8,6 @@ import type {
78
import { defineRouteContract } from '@/lib/api/contracts/types'
89
import { validateAwsRegion } from '@/lib/core/security/input-validation'
910

10-
/**
11-
* A trail name (3-128 chars, ASCII alphanumerics plus non-adjacent `.`, `_`, `-`,
12-
* starting and ending alphanumeric) or a full trail ARN. Shadow trails and
13-
* organization trails in another Region can only be addressed by ARN.
14-
* @see https://docs.aws.amazon.com/awscloudtrail/latest/APIReference/API_GetTrail.html
15-
*/
16-
const trailNameOrArnSchema = z
17-
.string()
18-
.trim()
19-
.min(3, 'Trail name must be at least 3 characters')
20-
.max(256, 'Trail name or ARN is too long')
21-
.regex(
22-
/^(?:arn:aws[a-zA-Z0-9-]*:cloudtrail:[a-z0-9-]+:\d{12}:trail\/[\w.\-/]+|[a-zA-Z0-9](?:[._-]?[a-zA-Z0-9]+)+)$/,
23-
'Must be a valid trail name or trail ARN'
24-
)
25-
2611
const advancedEventSelectorSchema = z.object({
2712
name: z.string().nullable(),
2813
fieldSelectors: z.array(
@@ -47,7 +32,7 @@ const GetEventSelectorsSchema = z.object({
4732
}),
4833
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
4934
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
50-
trailName: trailNameOrArnSchema,
35+
trailName: cloudtrailTrailNameOrArnSchema,
5136
})
5237

5338
const GetEventSelectorsResponseSchema = z.object({

‎apps/sim/lib/api/contracts/tools/aws/cloudtrail-get-insight-selectors.ts‎

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { z } from 'zod'
2+
import { cloudtrailTrailNameOrArnSchema } from '@/lib/api/contracts/tools/aws/cloudtrail-shared'
23
import type {
34
ContractBody,
45
ContractBodyInput,
@@ -7,22 +8,6 @@ import type {
78
import { defineRouteContract } from '@/lib/api/contracts/types'
89
import { validateAwsRegion } from '@/lib/core/security/input-validation'
910

10-
/**
11-
* A trail name (3-128 chars, ASCII alphanumerics plus non-adjacent `.`, `_`, `-`,
12-
* starting and ending alphanumeric) or a full trail ARN. Shadow trails and
13-
* organization trails in another Region can only be addressed by ARN.
14-
* @see https://docs.aws.amazon.com/awscloudtrail/latest/APIReference/API_GetTrail.html
15-
*/
16-
const trailNameOrArnSchema = z
17-
.string()
18-
.trim()
19-
.min(3, 'Trail name must be at least 3 characters')
20-
.max(256, 'Trail name or ARN is too long')
21-
.regex(
22-
/^(?:arn:aws[a-zA-Z0-9-]*:cloudtrail:[a-z0-9-]+:\d{12}:trail\/[\w.\-/]+|[a-zA-Z0-9](?:[._-]?[a-zA-Z0-9]+)+)$/,
23-
'Must be a valid trail name or trail ARN'
24-
)
25-
2611
/**
2712
* The ARN, or the ID suffix of the ARN, of a CloudTrail Lake event data store.
2813
* @see https://docs.aws.amazon.com/awscloudtrail/latest/APIReference/API_GetEventDataStore.html
@@ -44,7 +29,7 @@ const GetInsightSelectorsSchema = z
4429
}),
4530
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
4631
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
47-
trailName: trailNameOrArnSchema.optional(),
32+
trailName: cloudtrailTrailNameOrArnSchema.optional(),
4833
eventDataStore: eventDataStoreSchema.optional(),
4934
})
5035
.refine((v) => Boolean(v.trailName) !== Boolean(v.eventDataStore), {

‎apps/sim/lib/api/contracts/tools/aws/cloudtrail-get-trail-status.ts‎

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { z } from 'zod'
2+
import { cloudtrailTrailNameOrArnSchema } from '@/lib/api/contracts/tools/aws/cloudtrail-shared'
23
import type {
34
ContractBody,
45
ContractBodyInput,
@@ -7,22 +8,6 @@ import type {
78
import { defineRouteContract } from '@/lib/api/contracts/types'
89
import { validateAwsRegion } from '@/lib/core/security/input-validation'
910

10-
/**
11-
* A trail name (3-128 chars, ASCII alphanumerics plus non-adjacent `.`, `_`, `-`,
12-
* starting and ending alphanumeric) or a full trail ARN. Shadow trails and
13-
* organization trails in another Region can only be addressed by ARN.
14-
* @see https://docs.aws.amazon.com/awscloudtrail/latest/APIReference/API_GetTrail.html
15-
*/
16-
const trailNameOrArnSchema = z
17-
.string()
18-
.trim()
19-
.min(3, 'Trail name must be at least 3 characters')
20-
.max(256, 'Trail name or ARN is too long')
21-
.regex(
22-
/^(?:arn:aws[a-zA-Z0-9-]*:cloudtrail:[a-z0-9-]+:\d{12}:trail\/[\w.\-/]+|[a-zA-Z0-9](?:[._-]?[a-zA-Z0-9]+)+)$/,
23-
'Must be a valid trail name or trail ARN'
24-
)
25-
2611
const GetTrailStatusSchema = z.object({
2712
region: z
2813
.string()
@@ -32,7 +17,7 @@ const GetTrailStatusSchema = z.object({
3217
}),
3318
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
3419
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
35-
name: trailNameOrArnSchema,
20+
name: cloudtrailTrailNameOrArnSchema,
3621
})
3722

3823
const GetTrailStatusResponseSchema = z.object({

‎apps/sim/lib/api/contracts/tools/aws/cloudtrail-get-trail.ts‎

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { z } from 'zod'
2+
import { cloudtrailTrailNameOrArnSchema } from '@/lib/api/contracts/tools/aws/cloudtrail-shared'
23
import type {
34
ContractBody,
45
ContractBodyInput,
@@ -7,22 +8,6 @@ import type {
78
import { defineRouteContract } from '@/lib/api/contracts/types'
89
import { validateAwsRegion } from '@/lib/core/security/input-validation'
910

10-
/**
11-
* A trail name (3-128 chars, ASCII alphanumerics plus non-adjacent `.`, `_`, `-`,
12-
* starting and ending alphanumeric) or a full trail ARN. Shadow trails and
13-
* organization trails in another Region can only be addressed by ARN.
14-
* @see https://docs.aws.amazon.com/awscloudtrail/latest/APIReference/API_GetTrail.html
15-
*/
16-
const trailNameOrArnSchema = z
17-
.string()
18-
.trim()
19-
.min(3, 'Trail name must be at least 3 characters')
20-
.max(256, 'Trail name or ARN is too long')
21-
.regex(
22-
/^(?:arn:aws[a-zA-Z0-9-]*:cloudtrail:[a-z0-9-]+:\d{12}:trail\/[\w.\-/]+|[a-zA-Z0-9](?:[._-]?[a-zA-Z0-9]+)+)$/,
23-
'Must be a valid trail name or trail ARN'
24-
)
25-
2611
const GetTrailSchema = z.object({
2712
region: z
2813
.string()
@@ -32,7 +17,7 @@ const GetTrailSchema = z.object({
3217
}),
3318
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
3419
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
35-
name: trailNameOrArnSchema,
20+
name: cloudtrailTrailNameOrArnSchema,
3621
})
3722

3823
const GetTrailResponseSchema = z.object({
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { z } from 'zod'
2+
3+
/**
4+
* Boundary primitives shared by the AWS CloudTrail tool contracts.
5+
*
6+
* Every bound and pattern here is transcribed from the CloudTrail API Reference.
7+
*/
8+
9+
/** Longest trail name CloudTrail accepts, per `InvalidTrailNameException`. */
10+
const TRAIL_NAME_MAX_LENGTH = 128
11+
12+
/** Longest trail ARN CloudTrail accepts wherever a name or ARN is allowed. */
13+
const TRAIL_ARN_MAX_LENGTH = 256
14+
15+
const TRAIL_NAME_OR_ARN_PATTERN =
16+
/^(?:arn:aws[a-zA-Z0-9-]*:cloudtrail:[a-z0-9-]+:\d{12}:trail\/[\w.\-/]+|[a-zA-Z0-9](?:[._-]?[a-zA-Z0-9]+)+)$/
17+
18+
/**
19+
* A trail name or a full trail ARN. The two branches carry different ceilings: a bare name
20+
* is 3-128 characters of ASCII alphanumerics plus non-adjacent `.`, `_`, `-`, starting and
21+
* ending alphanumeric, while an ARN may run to 256. Applying only the ARN ceiling would let
22+
* a 129-256 character bare name through to AWS, which rejects it with
23+
* `InvalidTrailNameException`. Shadow trails and organization trails in another Region can
24+
* only be addressed by ARN.
25+
* @see https://docs.aws.amazon.com/awscloudtrail/latest/APIReference/API_GetTrail.html
26+
* @see https://docs.aws.amazon.com/awscloudtrail/latest/APIReference/API_DescribeTrails.html
27+
*/
28+
export const cloudtrailTrailNameOrArnSchema = z
29+
.string()
30+
.trim()
31+
.min(3, 'Trail name must be at least 3 characters')
32+
.max(TRAIL_ARN_MAX_LENGTH, 'Trail name or ARN is too long')
33+
.regex(TRAIL_NAME_OR_ARN_PATTERN, 'Must be a valid trail name or trail ARN')
34+
.superRefine((value, ctx) => {
35+
if (!value.startsWith('arn:') && value.length > TRAIL_NAME_MAX_LENGTH) {
36+
ctx.addIssue({
37+
code: 'custom',
38+
message: `Trail name must be at most ${TRAIL_NAME_MAX_LENGTH} characters; use the trail ARN to address a trail in another Region`,
39+
})
40+
}
41+
})

0 commit comments

Comments
 (0)