@@ -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"
1117import { 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"
1319import {
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