Skip to content

Commit 3dbef16

Browse files
committed
Add suppressions for CDK-generated Lambda handlers
1 parent 140101e commit 3dbef16

File tree

1 file changed

+156
-74
lines changed

1 file changed

+156
-74
lines changed

packages/cdk/stacks/EpsAssistMeStack.ts

Lines changed: 156 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,16 @@ import {
66
Fn,
77
CfnOutput
88
} from "aws-cdk-lib"
9-
import {Bucket, BucketEncryption, BlockPublicAccess} from "aws-cdk-lib/aws-s3"
10-
import * as AWSCDK from "aws-cdk-lib/aws-s3"
9+
import {
10+
Bucket,
11+
BucketEncryption,
12+
BlockPublicAccess,
13+
CfnBucket,
14+
CfnBucketPolicy
15+
} from "aws-cdk-lib/aws-s3"
16+
import {CfnFunction} from "aws-cdk-lib/aws-lambda"
1117
import {Key} from "aws-cdk-lib/aws-kms"
12-
import {AnyPrincipal, Effect, PolicyStatement} from "aws-cdk-lib/aws-iam"
18+
import {PolicyStatement} from "aws-cdk-lib/aws-iam"
1319
import {
1420
CfnGuardrail,
1521
CfnGuardrailVersion,
@@ -33,22 +39,22 @@ export class EpsAssistMeStack extends Stack {
3339
public constructor(scope: App, id: string, props: EpsAssistMeStackProps) {
3440
super(scope, id, props)
3541

36-
// Pull in context values from CLI or environment
42+
// ==== Context and constants ====
3743
const region = Stack.of(this).region
3844
const account = Stack.of(this).account
3945
const logRetentionInDays = Number(this.node.tryGetContext("logRetentionInDays")) || 14
4046
const logLevel: string = this.node.tryGetContext("logLevel")
4147
const slackBotToken: string = this.node.tryGetContext("slackBotToken")
4248
const slackSigningSecret: string = this.node.tryGetContext("slackSigningSecret")
4349

44-
// IAM and encryption key imports
50+
// ==== KMS Key Import ====
4551
const cloudWatchLogsKmsKey = Key.fromKeyArn(
4652
this,
4753
"cloudWatchLogsKmsKey",
4854
Fn.importValue("account-resources:CloudwatchLogsKmsKeyArn")
4955
)
5056

51-
// Access logs bucket
57+
// ==== Access Logs Bucket ====
5258
const accessLogBucket = new Bucket(this, "EpsAssistAccessLogsBucket", {
5359
encryption: BucketEncryption.KMS,
5460
encryptionKey: cloudWatchLogsKmsKey,
@@ -58,20 +64,8 @@ export class EpsAssistMeStack extends Stack {
5864
versioned: true
5965
})
6066

61-
// TLS-only policy
62-
accessLogBucket.addToResourcePolicy(new PolicyStatement({
63-
sid: "EnforceTLS",
64-
actions: ["s3:*"],
65-
effect: Effect.DENY,
66-
principals: [new AnyPrincipal()],
67-
resources: [accessLogBucket.bucketArn, `${accessLogBucket.bucketArn}/*`],
68-
conditions: {
69-
Bool: {"aws:SecureTransport": "false"}
70-
}
71-
}))
72-
73-
// Add replication config via escape hatch
74-
const accessLogBucketCfn = accessLogBucket.node.defaultChild as AWSCDK.CfnBucket
67+
// Add S3 replication and logging
68+
const accessLogBucketCfn = accessLogBucket.node.defaultChild as CfnBucket
7569
accessLogBucketCfn.replicationConfiguration = {
7670
role: `arn:aws:iam::${account}:role/account-resources-s3-replication-role`,
7771
rules: [{
@@ -83,8 +77,28 @@ export class EpsAssistMeStack extends Stack {
8377
deleteMarkerReplication: {status: "Disabled"}
8478
}]
8579
}
80+
accessLogBucketCfn.loggingConfiguration = {
81+
destinationBucketName: accessLogBucket.bucketName,
82+
logFilePrefix: "self-logs/"
83+
}
84+
85+
new CfnBucketPolicy(this, "AccessLogsBucketStrictTLSOnly", {
86+
bucket: accessLogBucket.bucketName,
87+
policyDocument: {
88+
Version: "2012-10-17",
89+
Statement: [{
90+
Action: "s3:*",
91+
Effect: "Deny",
92+
Principal: "*",
93+
Resource: "*",
94+
Condition: {
95+
Bool: {"aws:SecureTransport": false}
96+
}
97+
}]
98+
}
99+
})
86100

87-
// Secure document bucket
101+
// ==== Document Bucket ====
88102
const kbDocsBucket = new Bucket(this, "EpsAssistDocsBucket", {
89103
encryptionKey: cloudWatchLogsKmsKey,
90104
encryption: BucketEncryption.KMS,
@@ -96,20 +110,7 @@ export class EpsAssistMeStack extends Stack {
96110
serverAccessLogsPrefix: "s3-access-logs/"
97111
})
98112

99-
// Enforce TLS on S3 bucket
100-
kbDocsBucket.addToResourcePolicy(new PolicyStatement({
101-
sid: "EnforceTLS",
102-
actions: ["s3:*"],
103-
effect: Effect.DENY,
104-
principals: [new AnyPrincipal()],
105-
resources: [kbDocsBucket.bucketArn, `${kbDocsBucket.bucketArn}/*`],
106-
conditions: {
107-
Bool: {"aws:SecureTransport": "false"}
108-
}
109-
}))
110-
111-
// Add replication config via escape hatch
112-
const kbDocsBucketCfn = kbDocsBucket.node.defaultChild as AWSCDK.CfnBucket
113+
const kbDocsBucketCfn = kbDocsBucket.node.defaultChild as CfnBucket
113114
kbDocsBucketCfn.replicationConfiguration = {
114115
role: `arn:aws:iam::${account}:role/account-resources-s3-replication-role`,
115116
rules: [{
@@ -122,7 +123,23 @@ export class EpsAssistMeStack extends Stack {
122123
}]
123124
}
124125

125-
// Guardrails
126+
new CfnBucketPolicy(this, "KbDocsBucketStrictTLSOnly", {
127+
bucket: kbDocsBucket.bucketName,
128+
policyDocument: {
129+
Version: "2012-10-17",
130+
Statement: [{
131+
Action: "s3:*",
132+
Effect: "Deny",
133+
Principal: "*",
134+
Resource: "*",
135+
Condition: {
136+
Bool: {"aws:SecureTransport": false}
137+
}
138+
}]
139+
}
140+
})
141+
142+
// ==== Guardrail ====
126143
const guardrail = new CfnGuardrail(this, "EpsGuardrail", {
127144
name: "eps-assist-guardrail",
128145
description: "Guardrail for EPS Assist Me bot",
@@ -151,14 +168,13 @@ export class EpsAssistMeStack extends Stack {
151168
description: "Initial version of the EPS Assist Me Guardrail"
152169
})
153170

154-
// OpenSearch vector collection
171+
// ==== OpenSearch Vector Store ====
155172
const osCollection = new ops.CfnCollection(this, "OsCollection", {
156173
name: "eps-assist-vector-db",
157174
description: "EPS Assist Vector Store",
158175
type: "VECTORSEARCH"
159176
})
160177

161-
// OpenSearch encryption policy (AWS-owned key)
162178
new ops.CfnSecurityPolicy(this, "OsEncryptionPolicy", {
163179
name: "eps-assist-encryption-policy",
164180
type: "encryption",
@@ -168,54 +184,59 @@ export class EpsAssistMeStack extends Stack {
168184
})
169185
})
170186

171-
// OpenSearch network policy (allow public access for demo purposes)
172187
new ops.CfnSecurityPolicy(this, "OsNetworkPolicy", {
173188
name: "eps-assist-network-policy",
174189
type: "network",
175-
policy: JSON.stringify([
176-
{
177-
Rules: [
178-
{ResourceType: "collection", Resource: ["collection/eps-assist-vector-db"]},
179-
{ResourceType: "dashboard", Resource: ["collection/eps-assist-vector-db"]}
180-
],
181-
AllowFromPublic: true
182-
}
183-
])
190+
policy: JSON.stringify([{
191+
Rules: [
192+
{ResourceType: "collection", Resource: ["collection/eps-assist-vector-db"]},
193+
{ResourceType: "dashboard", Resource: ["collection/eps-assist-vector-db"]}
194+
],
195+
AllowFromPublic: true
196+
}])
184197
})
185198

186-
// CreateIndex Lambda
199+
// ==== Lambda Function: CreateIndex ====
187200
const createIndexFunction = new LambdaFunction(this, "CreateIndexFunction", {
188201
stackName: props.stackName,
189202
functionName: `${props.stackName}-CreateIndexFunction`,
190203
packageBasePath: "packages/createIndexFunction",
191204
entryPoint: "app.py",
192205
logRetentionInDays,
193206
logLevel,
194-
environmentVariables: {
195-
"INDEX_NAME": osCollection.attrId
196-
},
207+
environmentVariables: {"INDEX_NAME": osCollection.attrId},
197208
additionalPolicies: []
198209
})
199210

200-
// Access policy for Bedrock + Lambda to use the collection and index
211+
// Add cfn-guard suppressions for CreateIndex Lambda
212+
const createIndexLambdaCfn = createIndexFunction.function.node.defaultChild as CfnFunction
213+
createIndexLambdaCfn.cfnOptions.metadata = {
214+
guard: {
215+
SuppressedRules: [
216+
"LAMBDA_DLQ_CHECK",
217+
"LAMBDA_INSIDE_VPC",
218+
"LAMBDA_CONCURRENCY_CHECK"
219+
]
220+
}
221+
}
222+
223+
// ==== OpenSearch Access Policy ====
201224
new ops.CfnAccessPolicy(this, "OsAccessPolicy", {
202225
name: "eps-assist-access-policy",
203226
type: "data",
204-
policy: JSON.stringify([
205-
{
206-
Rules: [
207-
{ResourceType: "collection", Resource: ["collection/*"], Permission: ["aoss:*"]},
208-
{ResourceType: "index", Resource: ["index/*/*"], Permission: ["aoss:*"]}
209-
],
210-
Principal: [
211-
`arn:aws:iam::${account}:role/${createIndexFunction.function.role?.roleName}`,
212-
`arn:aws:iam::${account}:root`
213-
]
214-
}
215-
])
227+
policy: JSON.stringify([{
228+
Rules: [
229+
{ResourceType: "collection", Resource: ["collection/*"], Permission: ["aoss:*"]},
230+
{ResourceType: "index", Resource: ["index/*/*"], Permission: ["aoss:*"]}
231+
],
232+
Principal: [
233+
`arn:aws:iam::${account}:role/${createIndexFunction.function.role?.roleName}`,
234+
`arn:aws:iam::${account}:root`
235+
]
236+
}])
216237
})
217238

218-
// Trigger index creation using custom resource
239+
// ==== Trigger Vector Index Creation ====
219240
const endpoint = `${osCollection.attrId}.${region}.aoss.amazonaws.com`
220241
new cr.AwsCustomResource(this, "VectorIndex", {
221242
installLatestAwsSdk: true,
@@ -256,7 +277,7 @@ export class EpsAssistMeStack extends Stack {
256277
])
257278
})
258279

259-
// Knowledge Base that points to OpenSearch Serverless
280+
// ==== Bedrock Knowledge Base ====
260281
const kb = new CfnKnowledgeBase(this, "EpsKb", {
261282
name: "eps-assist-kb",
262283
description: "EPS Assist Knowledge Base",
@@ -281,7 +302,6 @@ export class EpsAssistMeStack extends Stack {
281302
}
282303
})
283304

284-
// Attach S3 data source to Knowledge Base
285305
new CfnDataSource(this, "EpsKbDataSource", {
286306
name: "eps-assist-kb-ds",
287307
knowledgeBaseId: kb.attrKnowledgeBaseId,
@@ -293,7 +313,7 @@ export class EpsAssistMeStack extends Stack {
293313
}
294314
})
295315

296-
// Lambda environment vars
316+
// ==== SlackBot Lambda ====
297317
const lambdaEnv: {[key: string]: string} = {
298318
RAG_MODEL_ID: "anthropic.claude-3-sonnet-20240229-v1:0",
299319
EMBEDDING_MODEL: "amazon.titan-embed-text-v2:0",
@@ -310,7 +330,6 @@ export class EpsAssistMeStack extends Stack {
310330
SLACK_SIGNING_SECRET: slackSigningSecret
311331
}
312332

313-
// SlackBot Lambda function
314333
const slackBotLambda = new LambdaFunction(this, "SlackBotLambda", {
315334
stackName: props.stackName,
316335
functionName: `${props.stackName}-SlackBotFunction`,
@@ -322,7 +341,35 @@ export class EpsAssistMeStack extends Stack {
322341
additionalPolicies: []
323342
})
324343

325-
// API Gateway
344+
const slackBotLambdaCfn = slackBotLambda.function.node.defaultChild as CfnFunction
345+
slackBotLambdaCfn.cfnOptions.metadata = {
346+
guard: {
347+
SuppressedRules: [
348+
"LAMBDA_DLQ_CHECK",
349+
"LAMBDA_INSIDE_VPC",
350+
"LAMBDA_CONCURRENCY_CHECK"
351+
]
352+
}
353+
}
354+
355+
// AwsCustomResource internal Lambda handler suppression
356+
const customResourceHandler = this.node
357+
.tryFindChild("VectorIndex")
358+
?.node.tryFindChild("CustomResourceProvider")
359+
?.node.tryFindChild("Handler")
360+
const customResourceHandlerCfn = customResourceHandler?.node.defaultChild as CfnFunction
361+
if (customResourceHandlerCfn) {
362+
customResourceHandlerCfn.cfnOptions.metadata = {
363+
guard: {
364+
SuppressedRules: [
365+
"LAMBDA_DLQ_CHECK",
366+
"LAMBDA_INSIDE_VPC"
367+
]
368+
}
369+
}
370+
}
371+
372+
// ==== API Gateway + Slack Route ====
326373
const apiGateway = new RestApiGateway(this, "EpsAssistApiGateway", {
327374
stackName: props.stackName,
328375
logRetentionInDays,
@@ -331,20 +378,55 @@ export class EpsAssistMeStack extends Stack {
331378
truststoreVersion: "unused"
332379
})
333380

334-
// API Route
335381
const slackRoute = apiGateway.api.root.addResource("slack").addResource("ask-eps")
336382
slackRoute.addMethod("POST", new LambdaIntegration(slackBotLambda.function, {
337383
credentialsRole: apiGateway.role
338384
}))
339385

340386
apiGateway.role.addManagedPolicy(slackBotLambda.executionPolicy)
341387

342-
// Output the SlackBot API endpoint
343388
new CfnOutput(this, "SlackBotEndpoint", {
344389
value: `https://${apiGateway.api.domainName?.domainName}/slack/ask-eps`
345390
})
346391

347-
// cdk-nag suppressions
392+
// ==== Suppressions for CDK-generated Lambda handlers ====
393+
// Suppress CDK-generated S3 AutoDeleteObjects handler
394+
const customS3Handler = Stack.of(this).node
395+
.tryFindChild("Custom::S3AutoDeleteObjectsCustomResourceProvider")
396+
?.node.tryFindChild("Handler")
397+
398+
const customS3HandlerCfn = customS3Handler?.node.defaultChild as CfnFunction
399+
400+
if (customS3HandlerCfn) {
401+
customS3HandlerCfn.cfnOptions.metadata = {
402+
guard: {
403+
SuppressedRules: [
404+
"LAMBDA_DLQ_CHECK",
405+
"LAMBDA_INSIDE_VPC"
406+
]
407+
}
408+
}
409+
}
410+
411+
// Suppress AWS679f... CDK-generated unnamed Lambda (e.g., AwsCustomResource)
412+
for (const construct of this.node.findAll()) {
413+
const fn = construct.node.defaultChild
414+
if (
415+
fn instanceof CfnFunction &&
416+
fn.logicalId.startsWith("AWS679f")
417+
) {
418+
fn.cfnOptions.metadata = {
419+
guard: {
420+
SuppressedRules: [
421+
"LAMBDA_DLQ_CHECK",
422+
"LAMBDA_INSIDE_VPC"
423+
]
424+
}
425+
}
426+
}
427+
}
428+
429+
// ==== Final CDK Nag Suppressions ====
348430
nagSuppressions(this)
349431
}
350432
}

0 commit comments

Comments
 (0)