Coverage for gco/stacks/nag_suppressions.py: 100%

88 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-06-15 15:07 +0000

1"""CDK-nag suppression utilities for GCO stacks. 

2 

3This module provides centralized suppression management for cdk-nag rules 

4that are intentionally not applicable or have documented justifications. 

5 

6Supported Compliance Frameworks: 

7- AWS Solutions: Best practices for AWS architectures 

8- HIPAA Security: Healthcare compliance requirements 

9- NIST 800-53 Rev 5: Federal security controls 

10- PCI DSS 3.2.1: Payment card industry standards 

11- Serverless: Best practices for serverless architectures 

12 

13Suppression Categories: 

141. AWS Managed Policies - Required for EKS/Lambda integrations 

152. Inline Policies - CDK-generated for custom resources 

163. Wildcard Permissions - Required for dynamic resource access 

174. Infrastructure Patterns - Intentional architectural decisions 

18""" 

19 

20from aws_cdk import Stack 

21from cdk_nag import NagPackSuppression, NagSuppressions 

22from constructs import IConstruct 

23 

24 

25def suppress_managed_policy_opt_in( 

26 resource: IConstruct, 

27 *, 

28 managed_policy_name: str, 

29 reason: str, 

30 apply_to_children: bool = False, 

31) -> None: 

32 """Scoped ``AwsSolutions-IAM4`` suppression for an intentional managed-policy attach. 

33 

34 The house pattern for GCO is to enumerate least-privilege 

35 statements rather than attach AWS-managed policies, but a handful 

36 of opt-in sub-features (e.g. SageMaker Canvas) *must* track a 

37 managed policy because the underlying service's per-feature 

38 permission surface evolves faster than we can keep up with. For 

39 those cases we accept the ``AwsSolutions-IAM4`` finding with a 

40 scoped suppression rather than a broad one. 

41 

42 This helper is the single call-site format for that pattern: pass 

43 the resource, the managed-policy name, and a one-line reason 

44 describing why the policy is appropriate for your feature. The 

45 helper expands the standard ``Policy::arn:<AWS::Partition>:iam:: 

46 aws:policy/<name>`` applies-to ARN so every managed-policy opt-in 

47 in the codebase uses the same suppression shape — reviewers can 

48 grep for ``suppress_managed_policy_opt_in(`` to find every one. 

49 

50 Args: 

51 resource: The IAM role (or other CDK construct) receiving the 

52 managed-policy attachment. 

53 managed_policy_name: The bare managed-policy name (e.g. 

54 ``"AmazonSageMakerCanvasFullAccess"``). Must NOT include 

55 the ``arn:<partition>:iam::aws:policy/`` prefix — the 

56 helper adds that. 

57 reason: Human-readable justification. Must explain (a) why 

58 the managed policy is preferred over an enumerated 

59 least-privilege policy, and (b) what the toggle or 

60 conditional is that gates the attachment (so reviewers 

61 can confirm the wider permission surface is opt-in). 

62 apply_to_children: Forward to 

63 ``NagSuppressions.add_resource_suppressions``. 

64 """ 

65 NagSuppressions.add_resource_suppressions( 

66 resource, 

67 [ 

68 NagPackSuppression( 

69 id="AwsSolutions-IAM4", 

70 reason=reason, 

71 applies_to=[ 

72 f"Policy::arn:<AWS::Partition>:iam::aws:policy/{managed_policy_name}", 

73 ], 

74 ), 

75 ], 

76 apply_to_children=apply_to_children, 

77 ) 

78 

79 

80def add_eks_suppressions(stack: Stack) -> None: 

81 """Add suppressions for EKS-related cdk-nag findings. 

82 

83 EKS requires specific AWS managed policies that cannot be replaced 

84 with customer-managed policies without breaking functionality. 

85 """ 

86 # EKS requires these AWS managed policies - they are AWS-recommended 

87 eks_managed_policies = [ 

88 "Policy::arn:<AWS::Partition>:iam::aws:policy/AmazonEKSClusterPolicy", 

89 "Policy::arn:<AWS::Partition>:iam::aws:policy/AmazonEKSComputePolicy", 

90 "Policy::arn:<AWS::Partition>:iam::aws:policy/AmazonEKSBlockStoragePolicy", 

91 "Policy::arn:<AWS::Partition>:iam::aws:policy/AmazonEKSLoadBalancingPolicy", 

92 "Policy::arn:<AWS::Partition>:iam::aws:policy/AmazonEKSNetworkingPolicy", 

93 "Policy::arn:<AWS::Partition>:iam::aws:policy/AmazonEKSWorkerNodePolicy", 

94 "Policy::arn:<AWS::Partition>:iam::aws:policy/AmazonEKS_CNI_Policy", 

95 "Policy::arn:<AWS::Partition>:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly", 

96 "Policy::arn:<AWS::Partition>:iam::aws:policy/service-role/AmazonEFSCSIDriverPolicy", 

97 # CloudWatch Observability addon policies for Container Insights 

98 "Policy::arn:<AWS::Partition>:iam::aws:policy/CloudWatchAgentServerPolicy", 

99 "Policy::arn:<AWS::Partition>:iam::aws:policy/AWSXrayWriteOnlyAccess", 

100 ] 

101 

102 NagSuppressions.add_stack_suppressions( 

103 stack, 

104 [ 

105 NagPackSuppression( 

106 id="AwsSolutions-IAM4", 

107 reason=( 

108 "EKS requires AWS managed policies for cluster, node, and add-on functionality. " 

109 "These are AWS-recommended policies that provide necessary permissions for EKS Auto Mode. " 

110 "See: https://docs.aws.amazon.com/eks/latest/userguide/security-iam-awsmanpol.html" 

111 ), 

112 applies_to=eks_managed_policies, 

113 ), 

114 ], 

115 ) 

116 

117 

118def add_lambda_suppressions(stack: Stack) -> None: 

119 """Add suppressions for Lambda-related cdk-nag findings. 

120 

121 Lambda functions used for CDK custom resources and infrastructure 

122 automation have specific requirements that trigger cdk-nag warnings. 

123 """ 

124 lambda_managed_policies = [ 

125 "Policy::arn:<AWS::Partition>:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", 

126 "Policy::arn:<AWS::Partition>:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole", 

127 ] 

128 

129 NagSuppressions.add_stack_suppressions( 

130 stack, 

131 [ 

132 NagPackSuppression( 

133 id="AwsSolutions-IAM4", 

134 reason=( 

135 "Lambda basic execution and VPC access roles are AWS-recommended managed policies. " 

136 "They provide minimal permissions for CloudWatch Logs and VPC ENI management. " 

137 "See: https://docs.aws.amazon.com/lambda/latest/dg/lambda-intro-execution-role.html" 

138 ), 

139 applies_to=lambda_managed_policies, 

140 ), 

141 NagPackSuppression( 

142 id="AwsSolutions-L1", 

143 reason=( 

144 "CDK Provider framework Lambda functions use a specific runtime version " 

145 "managed by CDK. These are internal functions not exposed to users." 

146 ), 

147 ), 

148 # HIPAA Lambda suppressions 

149 NagPackSuppression( 

150 id="HIPAA.Security-LambdaConcurrency", 

151 reason=( 

152 "Infrastructure Lambda functions (custom resources) are invoked only during " 

153 "stack deployment and do not require concurrency limits. They are not user-facing." 

154 ), 

155 ), 

156 NagPackSuppression( 

157 id="HIPAA.Security-LambdaDLQ", 

158 reason=( 

159 "CDK custom resource Lambda functions have built-in retry logic and report " 

160 "failures directly to CloudFormation. DLQ is not applicable for this pattern." 

161 ), 

162 ), 

163 NagPackSuppression( 

164 id="HIPAA.Security-LambdaInsideVPC", 

165 reason=( 

166 "CDK Provider framework Lambda functions need internet access to communicate " 

167 "with CloudFormation. VPC placement would require NAT Gateway configuration. " 

168 "User-facing Lambda functions (kubectl applier) ARE placed in VPC." 

169 ), 

170 ), 

171 # NIST 800-53 Lambda suppressions 

172 NagPackSuppression( 

173 id="NIST.800.53.R5-LambdaConcurrency", 

174 reason=( 

175 "Infrastructure Lambda functions (custom resources) are invoked only during " 

176 "stack deployment and do not require concurrency limits." 

177 ), 

178 ), 

179 NagPackSuppression( 

180 id="NIST.800.53.R5-LambdaDLQ", 

181 reason=( 

182 "CDK custom resource Lambda functions have built-in retry logic and report " 

183 "failures directly to CloudFormation. DLQ is not applicable." 

184 ), 

185 ), 

186 NagPackSuppression( 

187 id="NIST.800.53.R5-LambdaInsideVPC", 

188 reason=( 

189 "CDK Provider framework Lambda functions need internet access to communicate " 

190 "with CloudFormation. User-facing Lambda functions ARE placed in VPC." 

191 ), 

192 ), 

193 # PCI DSS Lambda suppressions 

194 NagPackSuppression( 

195 id="PCI.DSS.321-LambdaInsideVPC", 

196 reason=( 

197 "CDK Provider framework Lambda functions need internet access to communicate " 

198 "with CloudFormation. User-facing Lambda functions ARE placed in VPC." 

199 ), 

200 ), 

201 # Serverless Lambda suppressions 

202 NagPackSuppression( 

203 id="Serverless-LambdaLatestVersion", 

204 reason=( 

205 "CDK Provider framework Lambda functions use a specific runtime version " 

206 "managed by CDK. These are internal functions not exposed to users." 

207 ), 

208 ), 

209 NagPackSuppression( 

210 id="Serverless-LambdaDefaultMemorySize", 

211 reason=( 

212 "CDK Provider framework Lambda functions have appropriate memory for their " 

213 "workload. Custom Lambda functions have explicit memory configuration." 

214 ), 

215 ), 

216 NagPackSuppression( 

217 id="Serverless-LambdaDLQ", 

218 reason=( 

219 "CDK custom resource Lambda functions have built-in retry logic and report " 

220 "failures directly to CloudFormation. DLQ is not applicable." 

221 ), 

222 ), 

223 ], 

224 ) 

225 

226 

227def add_iam_suppressions( 

228 stack: Stack, regions: list[str] | None = None, global_region: str | None = None 

229) -> None: 

230 """Add suppressions for IAM-related cdk-nag findings. 

231 

232 CDK generates inline policies for custom resources and some patterns 

233 require wildcard permissions for dynamic resource access. 

234 

235 Args: 

236 stack: The CDK stack to apply suppressions to 

237 regions: List of regional deployment regions (for EKS addon patterns) 

238 global_region: Global region for SSM parameters and DynamoDB tables 

239 """ 

240 # Build dynamic applies_to list based on configured regions 

241 applies_to = [ 

242 "Resource::<KubectlApplierFunction6147DA0C.Arn>:*", 

243 "Resource::<GaRegistrationFunction4A12C41B.Arn>:*", 

244 "Resource::<HelmInstallerFunction3FEB04EF.Arn>:*", 

245 "Resource::<VpcFlowLogGroup86559C69.Arn>:*", 

246 # Secrets Manager cross-region access with wildcard for suffix 

247 f"Resource::arn:aws:secretsmanager:{global_region or 'us-east-2'}:<AWS::AccountId>:secret:gco/api-gateway-auth-token*", 

248 ] 

249 

250 # Add EKS addon patterns for each configured region 

251 if regions: 

252 for region in regions: 

253 applies_to.append( 

254 f"Resource::arn:aws:eks:{region}:<AWS::AccountId>:addon/<GCOEksCluster841A896A>/*" 

255 ) 

256 

257 # Add SSM parameter patterns for global region and all regional regions. 

258 # Using ``dict.fromkeys`` (insertion-ordered) + sorting gives a stable 

259 # ordering so the cdk-nag metadata block doesn't churn between synths 

260 # when PYTHONHASHSEED changes — previous ``set()`` iteration order was 

261 # hash-based and produced non-deterministic template diffs. 

262 ssm_regions_set: set[str] = set() 

263 if global_region: 

264 ssm_regions_set.add(global_region) 

265 if regions: 

266 ssm_regions_set.update(regions) 

267 

268 for region in sorted(ssm_regions_set): 

269 applies_to.append(f"Resource::arn:aws:ssm:{region}:<AWS::AccountId>:parameter/gco/*") 

270 

271 # Add DynamoDB index wildcard patterns for global region 

272 # Tables are created in global stack, accessed from all regional stacks 

273 if global_region: 

274 applies_to.extend( 

275 [ 

276 f"Resource::arn:aws:dynamodb:{global_region}:<AWS::AccountId>:table/gco-job-templates/index/*", 

277 f"Resource::arn:aws:dynamodb:{global_region}:<AWS::AccountId>:table/gco-webhooks/index/*", 

278 f"Resource::arn:aws:dynamodb:{global_region}:<AWS::AccountId>:table/gco-jobs/index/*", 

279 f"Resource::arn:aws:dynamodb:{global_region}:<AWS::AccountId>:table/gco-inference-endpoints/index/*", 

280 ] 

281 ) 

282 

283 # Add S3 wildcard patterns for model weights bucket 

284 # Bucket name is auto-generated by CDK, so we use a prefix pattern 

285 applies_to.extend( 

286 [ 

287 "Resource::arn:aws:s3:::gco-*", 

288 "Resource::arn:aws:s3:::gco-*/*", 

289 ] 

290 ) 

291 

292 # KMS wildcard scoped to S3 via condition for model weights bucket decryption 

293 applies_to.append("Resource::arn:aws:kms:*:<AWS::AccountId>:key/*") 

294 

295 NagSuppressions.add_stack_suppressions( 

296 stack, 

297 [ 

298 # Inline policy suppressions for all frameworks 

299 NagPackSuppression( 

300 id="HIPAA.Security-IAMNoInlinePolicy", 

301 reason=( 

302 "CDK generates inline policies for custom resources and Lambda functions. " 

303 "These are scoped to specific resources and follow least-privilege principles." 

304 ), 

305 ), 

306 NagPackSuppression( 

307 id="NIST.800.53.R5-IAMNoInlinePolicy", 

308 reason=( 

309 "CDK generates inline policies for custom resources and Lambda functions. " 

310 "These are scoped to specific resources and follow least-privilege principles." 

311 ), 

312 ), 

313 NagPackSuppression( 

314 id="PCI.DSS.321-IAMNoInlinePolicy", 

315 reason=( 

316 "CDK generates inline policies for custom resources and Lambda functions. " 

317 "These are scoped to specific resources and follow least-privilege principles." 

318 ), 

319 ), 

320 # Wildcard permission suppressions 

321 NagPackSuppression( 

322 id="AwsSolutions-IAM5", 

323 reason=( 

324 "Wildcard permissions are required for: (1) EKS cluster admin access to manage " 

325 "dynamic Kubernetes resources, (2) Custom resource providers to invoke Lambda versions, " 

326 "(3) SSM parameter access for cross-region coordination, (4) EKS addon management, " 

327 "(5) VPC Flow Logs to write to CloudWatch, (6) Secrets Manager cross-region access " 

328 "with wildcard suffix for auth token, (7) DynamoDB GSI access for job queue, templates, " 

329 "webhooks, and inference endpoints tables, (8) S3 access for model weights bucket " 

330 "(auto-generated name). All wildcards are scoped to specific patterns. " 

331 "(9) KMS decrypt scoped to S3 via condition for model weights bucket." 

332 ), 

333 applies_to=applies_to, 

334 ), 

335 ], 

336 ) 

337 

338 

339def add_vpc_suppressions(stack: Stack) -> None: 

340 """Add suppressions for VPC-related cdk-nag findings. 

341 

342 Public subnets and IGW routes are required for ALB and NAT Gateway 

343 functionality in a multi-tier architecture. 

344 """ 

345 NagSuppressions.add_stack_suppressions( 

346 stack, 

347 [ 

348 # HIPAA VPC suppressions 

349 NagPackSuppression( 

350 id="HIPAA.Security-VPCSubnetAutoAssignPublicIpDisabled", 

351 reason=( 

352 "Public subnets are required for internet-facing ALB. EC2 instances " 

353 "(EKS nodes) are deployed only in private subnets." 

354 ), 

355 ), 

356 NagPackSuppression( 

357 id="HIPAA.Security-VPCNoUnrestrictedRouteToIGW", 

358 reason=( 

359 "Public subnets require IGW route for ALB to receive traffic from " 

360 "Global Accelerator. All compute resources are in private subnets." 

361 ), 

362 ), 

363 # NIST 800-53 VPC suppressions 

364 NagPackSuppression( 

365 id="NIST.800.53.R5-VPCSubnetAutoAssignPublicIpDisabled", 

366 reason=( 

367 "Public subnets are required for internet-facing ALB. EC2 instances " 

368 "(EKS nodes) are deployed only in private subnets." 

369 ), 

370 ), 

371 NagPackSuppression( 

372 id="NIST.800.53.R5-VPCNoUnrestrictedRouteToIGW", 

373 reason=( 

374 "Public subnets require IGW route for ALB to receive traffic from " 

375 "Global Accelerator. All compute resources are in private subnets." 

376 ), 

377 ), 

378 # PCI DSS VPC suppressions 

379 NagPackSuppression( 

380 id="PCI.DSS.321-VPCSubnetAutoAssignPublicIpDisabled", 

381 reason=( 

382 "Public subnets are required for internet-facing ALB. EC2 instances " 

383 "(EKS nodes) are deployed only in private subnets." 

384 ), 

385 ), 

386 NagPackSuppression( 

387 id="PCI.DSS.321-VPCNoUnrestrictedRouteToIGW", 

388 reason=( 

389 "Public subnets require IGW route for ALB to receive traffic from " 

390 "Global Accelerator. All compute resources are in private subnets." 

391 ), 

392 ), 

393 ], 

394 ) 

395 

396 

397def add_api_gateway_suppressions(stack: Stack) -> None: 

398 """Add suppressions for API Gateway-related cdk-nag findings.""" 

399 NagSuppressions.add_stack_suppressions( 

400 stack, 

401 [ 

402 NagPackSuppression( 

403 id="AwsSolutions-COG4", 

404 reason=( 

405 "API Gateway uses IAM authentication (SigV4) instead of Cognito. " 

406 "This is intentional for machine-to-machine API access patterns." 

407 ), 

408 ), 

409 NagPackSuppression( 

410 id="AwsSolutions-APIG2", 

411 reason=( 

412 "Request validation is performed by the backend Manifest Processor service " 

413 "which has detailed schema validation. API Gateway acts as a pass-through proxy." 

414 ), 

415 ), 

416 # Cache suppressions - caching is intentionally disabled 

417 NagPackSuppression( 

418 id="HIPAA.Security-APIGWCacheEnabledAndEncrypted", 

419 reason=( 

420 "Caching is disabled intentionally. Manifest submissions are unique " 

421 "and should not be cached. Health checks need real-time data." 

422 ), 

423 ), 

424 NagPackSuppression( 

425 id="NIST.800.53.R5-APIGWCacheEnabledAndEncrypted", 

426 reason=( 

427 "Caching is disabled intentionally. Manifest submissions are unique " 

428 "and should not be cached. Health checks need real-time data." 

429 ), 

430 ), 

431 NagPackSuppression( 

432 id="PCI.DSS.321-APIGWCacheEnabledAndEncrypted", 

433 reason=( 

434 "Caching is disabled intentionally. Manifest submissions are unique " 

435 "and should not be cached. Health checks need real-time data." 

436 ), 

437 ), 

438 # SSL certificate suppressions 

439 NagPackSuppression( 

440 id="HIPAA.Security-APIGWSSLEnabled", 

441 reason=( 

442 "Backend SSL certificates are not required as traffic flows through " 

443 "Global Accelerator (TLS terminated) to internal ALB (HTTPS)." 

444 ), 

445 ), 

446 NagPackSuppression( 

447 id="NIST.800.53.R5-APIGWSSLEnabled", 

448 reason=( 

449 "Backend SSL certificates are not required as traffic flows through " 

450 "Global Accelerator (TLS terminated) to internal ALB (HTTPS)." 

451 ), 

452 ), 

453 NagPackSuppression( 

454 id="PCI.DSS.321-APIGWSSLEnabled", 

455 reason=( 

456 "Backend SSL certificates are not required as traffic flows through " 

457 "Global Accelerator (TLS terminated) to internal ALB (HTTPS)." 

458 ), 

459 ), 

460 # CloudWatch Log Group encryption suppressions 

461 NagPackSuppression( 

462 id="HIPAA.Security-CloudWatchLogGroupEncrypted", 

463 reason=( 

464 "CloudWatch Logs are encrypted by default with AWS-managed keys. " 

465 "Customer-managed KMS keys can be enabled via configuration if required." 

466 ), 

467 ), 

468 NagPackSuppression( 

469 id="NIST.800.53.R5-CloudWatchLogGroupEncrypted", 

470 reason=( 

471 "CloudWatch Logs are encrypted by default with AWS-managed keys. " 

472 "Customer-managed KMS keys can be enabled via configuration if required." 

473 ), 

474 ), 

475 NagPackSuppression( 

476 id="PCI.DSS.321-CloudWatchLogGroupEncrypted", 

477 reason=( 

478 "CloudWatch Logs are encrypted by default with AWS-managed keys. " 

479 "Customer-managed KMS keys can be enabled via configuration if required." 

480 ), 

481 ), 

482 # API Gateway CloudWatch role 

483 NagPackSuppression( 

484 id="AwsSolutions-IAM4", 

485 reason=( 

486 "API Gateway CloudWatch role requires the AWS managed policy " 

487 "AmazonAPIGatewayPushToCloudWatchLogs for logging functionality." 

488 ), 

489 applies_to=[ 

490 "Policy::arn:<AWS::Partition>:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs", 

491 ], 

492 ), 

493 # CdkNagValidationFailure for structured logging check 

494 NagPackSuppression( 

495 id="CdkNagValidationFailure", 

496 reason=( 

497 "Validation failure due to CloudFormation intrinsic functions. " 

498 "Access logging is properly configured on the API Gateway stage." 

499 ), 

500 ), 

501 ], 

502 ) 

503 

504 

505def add_monitoring_suppressions(stack: Stack) -> None: 

506 """Add suppressions for monitoring-related cdk-nag findings.""" 

507 NagSuppressions.add_stack_suppressions( 

508 stack, 

509 [ 

510 NagPackSuppression( 

511 id="AwsSolutions-SNS3", 

512 reason="SNS topic has enforce_ssl=True enabled, which adds the required policy.", 

513 ), 

514 NagPackSuppression( 

515 id="HIPAA.Security-SNSEncryptedKMS", 

516 reason=( 

517 "Alert notifications contain operational data (alarm names, thresholds) " 

518 "not PHI. KMS encryption adds latency to time-sensitive alerts." 

519 ), 

520 ), 

521 NagPackSuppression( 

522 id="NIST.800.53.R5-SNSEncryptedKMS", 

523 reason=( 

524 "Alert notifications contain operational data (alarm names, thresholds). " 

525 "KMS encryption adds latency to time-sensitive alerts." 

526 ), 

527 ), 

528 NagPackSuppression( 

529 id="PCI.DSS.321-SNSEncryptedKMS", 

530 reason=( 

531 "Alert notifications contain operational data (alarm names, thresholds). " 

532 "KMS encryption can be enabled if required for PCI compliance." 

533 ), 

534 ), 

535 # CloudWatch Log Group encryption 

536 NagPackSuppression( 

537 id="HIPAA.Security-CloudWatchLogGroupEncrypted", 

538 reason="CloudWatch Logs are encrypted by default with AWS-managed keys.", 

539 ), 

540 NagPackSuppression( 

541 id="NIST.800.53.R5-CloudWatchLogGroupEncrypted", 

542 reason="CloudWatch Logs are encrypted by default with AWS-managed keys.", 

543 ), 

544 NagPackSuppression( 

545 id="PCI.DSS.321-CloudWatchLogGroupEncrypted", 

546 reason="CloudWatch Logs are encrypted by default with AWS-managed keys.", 

547 ), 

548 # CloudWatch Alarm Action suppressions for composite alarm inputs 

549 # These alarms are intentionally used only as inputs to composite alarms 

550 # The composite alarms have actions attached, not the individual alarms 

551 NagPackSuppression( 

552 id="HIPAA.Security-CloudWatchAlarmAction", 

553 reason=( 

554 "These alarms are inputs to composite alarms which have SNS actions. " 

555 "Individual alarms don't need actions as they're aggregated for better signal-to-noise." 

556 ), 

557 ), 

558 NagPackSuppression( 

559 id="NIST.800.53.R5-CloudWatchAlarmAction", 

560 reason=( 

561 "These alarms are inputs to composite alarms which have SNS actions. " 

562 "Individual alarms don't need actions as they're aggregated for better signal-to-noise." 

563 ), 

564 ), 

565 ], 

566 ) 

567 

568 

569def add_storage_suppressions(stack: Stack) -> None: 

570 """Add suppressions for storage-related cdk-nag findings.""" 

571 NagSuppressions.add_stack_suppressions( 

572 stack, 

573 [ 

574 # EFS backup suppressions 

575 NagPackSuppression( 

576 id="HIPAA.Security-EFSInBackupPlan", 

577 reason=( 

578 "EFS backup is optional and can be enabled via AWS Backup if required. " 

579 "Default deployment prioritizes cost optimization." 

580 ), 

581 ), 

582 NagPackSuppression( 

583 id="NIST.800.53.R5-EFSInBackupPlan", 

584 reason=( 

585 "EFS backup is optional and can be enabled via AWS Backup if required. " 

586 "Default deployment prioritizes cost optimization." 

587 ), 

588 ), 

589 # CloudWatch Log Group encryption 

590 NagPackSuppression( 

591 id="HIPAA.Security-CloudWatchLogGroupEncrypted", 

592 reason=( 

593 "CloudWatch Logs are encrypted by default with AWS-managed keys. " 

594 "CDK Provider log groups are for infrastructure automation only." 

595 ), 

596 ), 

597 NagPackSuppression( 

598 id="NIST.800.53.R5-CloudWatchLogGroupEncrypted", 

599 reason=( 

600 "CloudWatch Logs are encrypted by default with AWS-managed keys. " 

601 "CDK Provider log groups are for infrastructure automation only." 

602 ), 

603 ), 

604 NagPackSuppression( 

605 id="PCI.DSS.321-CloudWatchLogGroupEncrypted", 

606 reason=( 

607 "CloudWatch Logs are encrypted by default with AWS-managed keys. " 

608 "CDK Provider log groups are for infrastructure automation only." 

609 ), 

610 ), 

611 ], 

612 ) 

613 

614 

615def add_sqs_suppressions(stack: Stack) -> None: 

616 """Add suppressions for SQS-related cdk-nag findings.""" 

617 NagSuppressions.add_stack_suppressions( 

618 stack, 

619 [ 

620 NagPackSuppression( 

621 id="AwsSolutions-SQS4", 

622 reason="SQS queues have enforce_ssl=True enabled, which adds the required policy.", 

623 ), 

624 NagPackSuppression( 

625 id="Serverless-SQSRedrivePolicy", 

626 reason=( 

627 "The dead-letter queue itself does not need a redrive policy. " 

628 "The main job queue has a redrive policy pointing to the DLQ." 

629 ), 

630 ), 

631 ], 

632 ) 

633 

634 

635def add_secrets_suppressions(stack: Stack) -> None: 

636 """Add suppressions for Secrets Manager-related cdk-nag findings.""" 

637 NagSuppressions.add_stack_suppressions( 

638 stack, 

639 [ 

640 # KMS key suppressions - using AWS-managed keys is acceptable 

641 NagPackSuppression( 

642 id="HIPAA.Security-SecretsManagerUsingKMSKey", 

643 reason=( 

644 "Secrets Manager encrypts secrets by default with AWS-managed keys. " 

645 "Customer-managed KMS can be enabled if required for compliance." 

646 ), 

647 ), 

648 NagPackSuppression( 

649 id="NIST.800.53.R5-SecretsManagerUsingKMSKey", 

650 reason="Secrets Manager encrypts secrets by default with AWS-managed keys.", 

651 ), 

652 NagPackSuppression( 

653 id="PCI.DSS.321-SecretsManagerUsingKMSKey", 

654 reason=( 

655 "Secrets Manager encrypts secrets by default with AWS-managed keys. " 

656 "Customer-managed KMS can be enabled if required for PCI compliance." 

657 ), 

658 ), 

659 ], 

660 ) 

661 

662 

663def add_eks_cluster_suppressions(stack: Stack) -> None: 

664 """Add suppressions for EKS cluster-specific findings.""" 

665 NagSuppressions.add_stack_suppressions( 

666 stack, 

667 [ 

668 NagPackSuppression( 

669 id="AwsSolutions-EKS1", 

670 reason=( 

671 "EKS public endpoint is enabled for kubectl access from CI/CD pipelines " 

672 "and developer workstations. Access is controlled via IAM." 

673 ), 

674 ), 

675 # CdkNagValidationFailure suppressions for security group rules with intrinsic functions 

676 NagPackSuppression( 

677 id="CdkNagValidationFailure", 

678 reason=( 

679 "Security group rules use VPC CIDR block via CloudFormation intrinsic function. " 

680 "The rule restricts access to VPC CIDR only, which is secure." 

681 ), 

682 ), 

683 ], 

684 ) 

685 

686 

687def add_backup_suppressions(stack: Stack) -> None: 

688 """Add suppressions for AWS Backup-related cdk-nag findings.""" 

689 NagSuppressions.add_stack_suppressions( 

690 stack, 

691 [ 

692 NagPackSuppression( 

693 id="AwsSolutions-IAM4", 

694 reason=( 

695 "AWS Backup requires the AWSBackupServiceRolePolicyForBackup managed policy " 

696 "attached to the backup service role to perform backup operations on DynamoDB tables. " 

697 "This is the AWS-recommended policy for AWS Backup default service roles. " 

698 "See: https://docs.aws.amazon.com/aws-backup/latest/devguide/iam-service-roles.html" 

699 ), 

700 applies_to=[ 

701 "Policy::arn:<AWS::Partition>:iam::aws:policy/service-role/AWSBackupServiceRolePolicyForBackup", 

702 ], 

703 ), 

704 ], 

705 ) 

706 

707 

708def add_aurora_pgvector_suppressions(stack: Stack) -> None: 

709 """Add suppressions for Aurora pgvector-related cdk-nag findings. 

710 

711 Aurora Serverless v2 with pgvector triggers several compliance findings 

712 that are intentionally accepted for this deployment pattern. 

713 """ 

714 NagSuppressions.add_stack_suppressions( 

715 stack, 

716 [ 

717 # Secrets Manager KMS key — Aurora secret uses AWS-managed encryption 

718 NagPackSuppression( 

719 id="HIPAA.Security-SecretsManagerUsingKMSKey", 

720 reason=( 

721 "Aurora Serverless v2 credentials in Secrets Manager are encrypted with " 

722 "AWS-managed keys by default. Customer-managed KMS can be enabled if required." 

723 ), 

724 ), 

725 NagPackSuppression( 

726 id="NIST.800.53.R5-SecretsManagerUsingKMSKey", 

727 reason=( 

728 "Aurora Serverless v2 credentials in Secrets Manager are encrypted with " 

729 "AWS-managed keys by default." 

730 ), 

731 ), 

732 # Secrets Manager rotation — Aurora manages rotation via RDS integration 

733 NagPackSuppression( 

734 id="HIPAA.Security-SecretsManagerRotationEnabled", 

735 reason=( 

736 "Aurora manages credential rotation via the RDS integration with Secrets " 

737 "Manager. Manual rotation configuration is not required." 

738 ), 

739 ), 

740 NagPackSuppression( 

741 id="NIST.800.53.R5-SecretsManagerRotationEnabled", 

742 reason=( 

743 "Aurora manages credential rotation via the RDS integration with Secrets " 

744 "Manager. Manual rotation configuration is not required." 

745 ), 

746 ), 

747 # RDS in backup plan — Aurora has built-in continuous backups 

748 NagPackSuppression( 

749 id="HIPAA.Security-RDSInBackupPlan", 

750 reason=( 

751 "Aurora Serverless v2 has built-in continuous backups with point-in-time " 

752 "recovery. AWS Backup integration is optional and can be enabled if required." 

753 ), 

754 ), 

755 NagPackSuppression( 

756 id="NIST.800.53.R5-RDSInBackupPlan", 

757 reason=( 

758 "Aurora Serverless v2 has built-in continuous backups with point-in-time " 

759 "recovery. AWS Backup integration is optional." 

760 ), 

761 ), 

762 # RDS logging enabled — covered by cloudwatch_logs_exports=["postgresql"] 

763 # but some frameworks check for additional log types 

764 NagPackSuppression( 

765 id="HIPAA.Security-RDSLoggingEnabled", 

766 reason=( 

767 "PostgreSQL logs are exported to CloudWatch via cloudwatch_logs_exports. " 

768 "Aurora Serverless v2 does not support all log types available on provisioned instances." 

769 ), 

770 ), 

771 NagPackSuppression( 

772 id="NIST.800.53.R5-RDSLoggingEnabled", 

773 reason=( 

774 "PostgreSQL logs are exported to CloudWatch via cloudwatch_logs_exports. " 

775 "Aurora Serverless v2 does not support all log types available on provisioned instances." 

776 ), 

777 ), 

778 NagPackSuppression( 

779 id="PCI.DSS.321-RDSLoggingEnabled", 

780 reason=( 

781 "PostgreSQL logs are exported to CloudWatch via cloudwatch_logs_exports. " 

782 "Aurora Serverless v2 does not support all log types available on provisioned instances." 

783 ), 

784 ), 

785 # CloudWatch Log Group encryption for Aurora logs 

786 NagPackSuppression( 

787 id="HIPAA.Security-CloudWatchLogGroupEncrypted", 

788 reason=( 

789 "CloudWatch Logs for Aurora PostgreSQL are encrypted by default with " 

790 "AWS-managed keys. Customer-managed KMS can be enabled if required." 

791 ), 

792 ), 

793 NagPackSuppression( 

794 id="NIST.800.53.R5-CloudWatchLogGroupEncrypted", 

795 reason=( 

796 "CloudWatch Logs for Aurora PostgreSQL are encrypted by default with " 

797 "AWS-managed keys." 

798 ), 

799 ), 

800 NagPackSuppression( 

801 id="PCI.DSS.321-CloudWatchLogGroupEncrypted", 

802 reason=( 

803 "CloudWatch Logs for Aurora PostgreSQL are encrypted by default with " 

804 "AWS-managed keys." 

805 ), 

806 ), 

807 # Enhanced monitoring IAM role uses AWS managed policy 

808 NagPackSuppression( 

809 id="AwsSolutions-IAM4", 

810 reason=( 

811 "Aurora enhanced monitoring requires the AWS managed policy " 

812 "AmazonRDSEnhancedMonitoringRole for publishing OS-level metrics to CloudWatch. " 

813 "This is the AWS-recommended policy for RDS enhanced monitoring. " 

814 "See: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_Monitoring.OS.Enabling.html" 

815 ), 

816 applies_to=[ 

817 "Policy::arn:<AWS::Partition>:iam::aws:policy/service-role/AmazonRDSEnhancedMonitoringRole", 

818 ], 

819 ), 

820 ], 

821 ) 

822 

823 

824def add_sagemaker_suppressions( 

825 stack: Stack, 

826 api_gateway_region: str | None = None, 

827 global_region: str | None = None, 

828) -> None: 

829 """Add suppressions for SageMaker Studio Domain + execution role findings. 

830 

831 The analytics stack uses a private-VPC SageMaker Studio domain whose 

832 execution role needs wildcard access to a known set of ARN patterns: 

833 regional SQS job queues (one per regional stack, name pattern 

834 ``<project>-jobs-<region>``), GCO API Gateway GET routes (any REST API 

835 id under ``/prod/GET/api/v1/*``), and ``Cluster_Shared_Bucket`` objects 

836 resolved from cross-region SSM. Each wildcard is scoped on the literal 

837 patterns below so cdk-nag's ``AwsSolutions-IAM5`` check surfaces only 

838 the documented escape hatches. 

839 

840 Args: 

841 stack: The analytics stack to apply suppressions to. 

842 api_gateway_region: Concrete region where the API Gateway stack 

843 lives (used to resolve the execute-api ARN pattern). 

844 global_region: Concrete global region (used to resolve the 

845 KMS ``ViaService`` condition's service endpoint — the KMS 

846 decrypt ARN itself is ``*`` because the cluster-shared KMS 

847 key lives in a different stack). 

848 """ 

849 api_region = api_gateway_region or "*" 

850 gbl_region = global_region or "*" 

851 

852 applies_to: list[str | dict[str, str]] = [ 

853 # SageMaker execution role — SQS submit to any regional queue under 

854 # the project's ``<project>-jobs-*`` pattern. The SQS queue ARNs 

855 # are owned by the regional stacks and not directly importable. 

856 "Resource::arn:aws:sqs:*:<AWS::AccountId>:gco-jobs-*", 

857 # SageMaker execution role — ``ssm:GetParameter`` on the 

858 # Cluster_Shared_Bucket metadata parameters under 

859 # ``/gco/cluster-shared-bucket/*`` in the global region. The path 

860 # wildcard covers exactly three literal parameter names 

861 # (name / arn / region) defined by ``GCOGlobalStack``; the rest of 

862 # the ARN is fully scoped (global region + account). 

863 f"Resource::arn:aws:ssm:{gbl_region}:<AWS::AccountId>:parameter/gco/cluster-shared-bucket/*", 

864 # SageMaker execution role — execute-api on any REST API id 

865 # under ``/prod/*/api/v1/*`` and ``/prod/*/inference/*`` in the 

866 # api-gateway region. The concrete region value is templated in 

867 # so the nag match works regardless of which region the user 

868 # deploys to. The HTTP-method segment is ``*`` (instead of 

869 # pinning ``GET``) so notebooks can submit jobs, update 

870 # templates, and manage inference endpoints in addition to 

871 # read-only GETs. 

872 f"Resource::arn:aws:execute-api:{api_region}:<AWS::AccountId>:*/prod/*/api/v1/*", 

873 f"Resource::arn:aws:execute-api:{api_region}:<AWS::AccountId>:*/prod/*/inference/*", 

874 # KMS decrypt scoped by ``kms:ViaService=s3.<global-region>.amazonaws.com`` 

875 # condition — the resource ARN is unknown to this stack (cluster- 

876 # shared KMS key lives in the global region) so Resource::* is the 

877 # documented pattern, narrowed by the ViaService condition. 

878 "Resource::*", 

879 # S3 grant_read_write on Studio_Only_Bucket produces the AWS- 

880 # recommended set of S3 action wildcards. Each one covers a 

881 # closed, read-or-write intent on a single literal bucket ARN. 

882 "Action::s3:Abort*", 

883 "Action::s3:DeleteObject*", 

884 "Action::s3:GetBucket*", 

885 "Action::s3:GetObject*", 

886 "Action::s3:List*", 

887 # KMS grant_encrypt_decrypt on Analytics_KMS_Key produces the 

888 # AWS-recommended set of KMS action wildcards. Each covers a 

889 # single key ARN. 

890 "Action::kms:GenerateDataKey*", 

891 "Action::kms:ReEncrypt*", 

892 # Object-key wildcard on the literal Studio_Only_Bucket ARN — the 

893 # RW grant must cover every object key under the bucket. 

894 {"regex": r"/^Resource::<StudioOnlyBucket.*\.Arn>\/\*$/"}, 

895 # ``kms:ViaService`` condition-scoped wildcard on the cluster- 

896 # shared bucket's KMS key — only matched when s3 is the invoking 

897 # service in the global region. 

898 f"Condition::kms:ViaService:s3.{gbl_region}.amazonaws.com", 

899 # Studio UI actions — the execution role is assumed by the Studio 

900 # runtime and needs domain/space/app/user-profile wildcards to 

901 # render the IDE and manage notebook apps. 

902 f"Resource::arn:aws:sagemaker:{api_region}:<AWS::AccountId>:domain/*", 

903 f"Resource::arn:aws:sagemaker:{api_region}:<AWS::AccountId>:user-profile/*/*", 

904 f"Resource::arn:aws:sagemaker:{api_region}:<AWS::AccountId>:space/*/*", 

905 f"Resource::arn:aws:sagemaker:{api_region}:<AWS::AccountId>:app/*/*/*/*", 

906 # EMR Serverless — Studio discovers and manages EMR apps via these 

907 # actions. Resource::* is required because EMR Serverless does not 

908 # support resource-level scoping on most actions. 

909 "Action::emr-serverless:*", 

910 # SageMaker MLflow tracking servers + MLflow Apps. The 

911 # ``sagemaker-mlflow:*`` data-plane namespace is the one the 

912 # MLflow SDK talks to via SigV4 (``log_metric``, 

913 # ``log_artifact``, ``register_model``, etc.); the managed 

914 # ``AmazonSageMakerFullAccess`` policy covers the 

915 # ``sagemaker:*`` control-plane namespace (MLflow Apps, 

916 # Tracking Servers, Model Registry) but NOT the 

917 # ``sagemaker-mlflow`` service prefix, so this statement stays 

918 # inline and scoped to the api-gateway region + account. 

919 "Action::sagemaker-mlflow:*", 

920 f"Resource::arn:aws:sagemaker:{api_region}:<AWS::AccountId>:mlflow-tracking-server/*", 

921 f"Resource::arn:aws:sagemaker:{api_region}:<AWS::AccountId>:mlflow-app/*", 

922 # ``sts:GetCallerIdentity`` does not support resource-level 

923 # scoping; the MLflow SigV4 plug-in calls it on every request. 

924 "Action::sts:GetCallerIdentity", 

925 ] 

926 

927 NagSuppressions.add_stack_suppressions( 

928 stack, 

929 [ 

930 NagPackSuppression( 

931 id="AwsSolutions-IAM5", 

932 reason=( 

933 "SageMaker_Execution_Role uses wildcard ARNs and actions for: " 

934 "(1) SQS SendMessage on any regional job queue matching " 

935 "``<project>-jobs-<region>``, (2) execute-api:Invoke on " 

936 "any REST API id under /prod/*/api/v1/* and " 

937 "/prod/*/inference/* in the api-gateway region (all " 

938 "HTTP methods, so notebooks can submit jobs and " 

939 "manage inference endpoints in addition to read-only " 

940 "GETs), (3) KMS Decrypt/GenerateDataKey " 

941 "scoped by kms:ViaService=s3.<global-region>.amazonaws.com " 

942 "condition (the cluster-shared KMS key ARN is not known " 

943 "to the analytics stack — it lives in the global region), " 

944 "(4) S3 action wildcards (``s3:Abort*``, ``s3:DeleteObject*``, " 

945 "``s3:GetBucket*``, ``s3:GetObject*``, ``s3:List*``) produced " 

946 "by ``bucket.grant_read_write(role)`` on the literal " 

947 "Studio_Only_Bucket ARN, (5) KMS action wildcards " 

948 "(``kms:GenerateDataKey*``, ``kms:ReEncrypt*``) produced by " 

949 "``kms_key.grant_encrypt_decrypt(role)`` on the literal " 

950 "Analytics_KMS_Key ARN, (6) ``<StudioOnlyBucket.Arn>/*`` " 

951 "object-key wildcard on the single literal bucket, and " 

952 "(7) ``ssm:GetParameter`` on " 

953 "``/gco/cluster-shared-bucket/*`` in the global region " 

954 "(covers exactly three literal parameter names — " 

955 "name/arn/region — defined by ``GCOGlobalStack``; lets " 

956 "Studio notebooks resolve the shared-bucket metadata at " 

957 "runtime without a per-user export step), (8) " 

958 "``sagemaker-mlflow:*`` on MLflow tracking server and " 

959 "MLflow App ARN wildcards in the api-gateway region + " 

960 "account so notebooks can log experiments, runs, " 

961 "metrics, artifacts, and registered-model versions " 

962 "via the MLflow SDK's SigV4 plug-in (the companion " 

963 "``sagemaker:*Mlflow*`` / ``sagemaker:*ModelPackage*`` " 

964 "control-plane actions are now attached via the " 

965 "``AmazonSageMakerFullAccess`` managed policy instead " 

966 "of enumerated inline), and (9) " 

967 "``sts:GetCallerIdentity`` on ``*`` which is required " 

968 "by MLflow's SigV4 plug-in and does not support " 

969 "resource-level scoping. Each wildcard is scoped on a " 

970 "narrow literal pattern." 

971 ), 

972 applies_to=applies_to, 

973 ), 

974 # SageMaker execution role does not require MFA — callers reach 

975 # the role through Cognito-gated presigned URLs rather 

976 # than direct AssumeRole calls from operator terminals. 

977 NagPackSuppression( 

978 id="AwsSolutions-IAM4", 

979 reason=( 

980 "SageMaker_Execution_Role does not attach AWS managed " 

981 "policies. The role is assumed only by sagemaker.amazonaws.com " 

982 "and used exclusively by notebooks running inside the " 

983 "Studio domain." 

984 ), 

985 ), 

986 # The Studio domain itself — VpcOnly network mode is the 

987 # primary security control; additional HIPAA/NIST checks that 

988 # assume a customer-managed image (``AwsSolutions-SM2`` etc.) 

989 # are suppressed because this deployment intentionally uses 

990 # the stock AWS-published SageMaker Distribution images. 

991 NagPackSuppression( 

992 id="AwsSolutions-SM2", 

993 reason=( 

994 "The Studio domain uses AWS-published stock SageMaker " 

995 "Distribution images and does not define custom " 

996 "images or app image configs. Per-user EFS access points " 

997 "give POSIX isolation without a custom image." 

998 ), 

999 ), 

1000 NagPackSuppression( 

1001 id="AwsSolutions-SM3", 

1002 reason=( 

1003 "SageMaker Studio domain is provisioned with " 

1004 "``app_network_access_type=VpcOnly`` — all Studio traffic " 

1005 "stays on the analytics stack's private-isolated VPC. " 

1006 "Direct internet access is structurally unavailable." 

1007 ), 

1008 ), 

1009 ], 

1010 ) 

1011 

1012 # The separate SagemakerClusterSharedBucketGrant inline Policy (a 

1013 # sibling construct to the role, created by 

1014 # ``_grant_sagemaker_role_on_cluster_shared_bucket``) has its own 

1015 # ``<ReadClusterSharedBucketArn*.Parameter.Value>/*`` object-key 

1016 # wildcard on the literal cluster-shared bucket ARN resolved from 

1017 # cross-region SSM. Resource-level scoping isn't possible here — 

1018 # the parent role's resource suppression has ``apply_to_children`` 

1019 # semantics that only traverse CDK children, not siblings. 

1020 NagSuppressions.add_stack_suppressions( 

1021 stack, 

1022 [ 

1023 NagPackSuppression( 

1024 id="AwsSolutions-IAM5", 

1025 reason=( 

1026 "SagemakerClusterSharedBucketGrant attaches the RW " 

1027 "policy on the single literal Cluster_Shared_Bucket " 

1028 "ARN resolved from /gco/cluster-shared-bucket/arn. " 

1029 "The ``<arn>/*`` object-key wildcard covers every " 

1030 "object key inside the single always-on " 

1031 "gco-cluster-shared-<account>-<region> bucket, " 

1032 "identical in shape and intent to the regional stack's " 

1033 "analogous job-pod grant." 

1034 ), 

1035 applies_to=[ 

1036 { 

1037 "regex": r"/^Resource::<ReadClusterSharedBucketArn.*\.Parameter\.Value>\/\*$/" 

1038 }, 

1039 ], 

1040 ), 

1041 ], 

1042 ) 

1043 

1044 

1045def add_cognito_suppressions(stack: Stack) -> None: 

1046 """Add suppressions for Cognito user pool findings. 

1047 

1048 Most Cognito-related checks are handled by 

1049 ``advanced_security_mode=ENFORCED`` and the password-policy 

1050 configuration set on the pool itself. Only a small number of 

1051 structural findings need an explicit suppression — these are the ones 

1052 that don't apply to a machine-to-machine + presigned-URL model where 

1053 there is no hosted UI callback to harden. 

1054 """ 

1055 NagSuppressions.add_stack_suppressions( 

1056 stack, 

1057 [ 

1058 NagPackSuppression( 

1059 id="AwsSolutions-COG3", 

1060 reason=( 

1061 "The Cognito user pool has ``advanced_security_mode=ENFORCED`` " 

1062 "which provides adaptive risk-based authentication, replacing " 

1063 "the need for an additional MFA enforcement step at this level. " 

1064 "Admins add MFA via ``gco analytics users add --require-mfa`` " 

1065 "when required." 

1066 ), 

1067 ), 

1068 # COG2 is WARN-level: "The Cognito user pool does not require 

1069 # MFA." MFA is configured at the per-user level through the 

1070 # ``gco analytics users add --require-mfa`` CLI path rather 

1071 # than being enforced pool-wide; enforcing it at the pool 

1072 # level would lock out admins bootstrapping the first user 

1073 # during initial deploy. 

1074 NagPackSuppression( 

1075 id="AwsSolutions-COG2", 

1076 reason=( 

1077 "MFA is managed per-user through the ``gco analytics " 

1078 "users add --require-mfa`` CLI command rather than " 

1079 "enforced pool-wide. ``advanced_security_mode=ENFORCED`` " 

1080 "provides adaptive risk-based authentication that " 

1081 "triggers MFA challenges on suspicious sign-in attempts. " 

1082 "Pool-wide MFA enforcement would lock out the first " 

1083 "admin bootstrapping user during initial deploy." 

1084 ), 

1085 ), 

1086 ], 

1087 ) 

1088 

1089 

1090def add_analytics_vpc_suppressions(stack: Stack) -> None: 

1091 """Add suppressions for the analytics VPC and its endpoints. 

1092 

1093 The analytics VPC uses private subnets with NAT egress for notebook 

1094 internet access (pip install, git clone) plus a small public subnet 

1095 that hosts only the NAT gateway ENI. Findings on the public subnet 

1096 and IGW route are expected — no compute runs there. 

1097 """ 

1098 NagSuppressions.add_stack_suppressions( 

1099 stack, 

1100 [ 

1101 # Flow-logs suppressions — analytics VPC is private-isolated, 

1102 # has no IGW/NAT, and every egress path is a VPC endpoint. The 

1103 # service endpoints already emit CloudTrail data events that 

1104 # cover every packet-producing API call on the VPC. 

1105 NagPackSuppression( 

1106 id="AwsSolutions-VPC7", 

1107 reason=( 

1108 "The analytics VPC is private-isolated (no IGW, no " 

1109 "NAT Gateway). All egress flows through VPC interface/" 

1110 "gateway endpoints for SageMaker, S3, STS, Logs, ECR, " 

1111 "and EFS, each of which emits CloudTrail data events. " 

1112 "Flow logs would duplicate that telemetry at " 

1113 "significant storage cost without adding visibility." 

1114 ), 

1115 ), 

1116 NagPackSuppression( 

1117 id="HIPAA.Security-VPCFlowLogsEnabled", 

1118 reason=( 

1119 "The analytics VPC is private-isolated (no IGW, no " 

1120 "NAT Gateway). All egress flows through VPC interface/" 

1121 "gateway endpoints for SageMaker, S3, STS, Logs, ECR, " 

1122 "and EFS, each of which emits CloudTrail data events. " 

1123 "Flow logs would duplicate that telemetry at " 

1124 "significant storage cost without adding visibility." 

1125 ), 

1126 ), 

1127 NagPackSuppression( 

1128 id="NIST.800.53.R5-VPCFlowLogsEnabled", 

1129 reason=( 

1130 "The analytics VPC is private-isolated (no IGW, no " 

1131 "NAT Gateway). All egress flows through VPC interface/" 

1132 "gateway endpoints for SageMaker, S3, STS, Logs, ECR, " 

1133 "and EFS, each of which emits CloudTrail data events. " 

1134 "Flow logs would duplicate that telemetry at " 

1135 "significant storage cost without adding visibility." 

1136 ), 

1137 ), 

1138 NagPackSuppression( 

1139 id="PCI.DSS.321-VPCFlowLogsEnabled", 

1140 reason=( 

1141 "The analytics VPC is private-isolated (no IGW, no " 

1142 "NAT Gateway). All egress flows through VPC interface/" 

1143 "gateway endpoints for SageMaker, S3, STS, Logs, ECR, " 

1144 "and EFS, each of which emits CloudTrail data events. " 

1145 "Flow logs would duplicate that telemetry at " 

1146 "significant storage cost without adding visibility." 

1147 ), 

1148 ), 

1149 # CdkNagValidationFailure suppressions for the VPC endpoint 

1150 # security-group rules — cdk-nag can't resolve the VPC CIDR 

1151 # block at synth time because it's an ``Fn::GetAtt`` token. 

1152 # The regional stack handles the same pattern via 

1153 # ``add_eks_cluster_suppressions``. 

1154 NagPackSuppression( 

1155 id="CdkNagValidationFailure", 

1156 reason=( 

1157 "VPC interface-endpoint security-group rules reference " 

1158 "the VPC CIDR block via ``Fn::GetAtt``. The Token " 

1159 "doesn't resolve at synth time so cdk-nag cannot " 

1160 "validate the rule; the rule itself is scoped to the " 

1161 "VPC CIDR, which is the tightest possible source for " 

1162 "intra-VPC endpoint traffic." 

1163 ), 

1164 ), 

1165 # Public subnet findings — the NAT gateway requires a public 

1166 # subnet with an IGW route. No compute runs in the public 

1167 # subnet; it only hosts the NAT gateway's ENI. 

1168 NagPackSuppression( 

1169 id="HIPAA.Security-VPCSubnetAutoAssignPublicIpDisabled", 

1170 reason=( 

1171 "The public subnet exists solely to host the NAT " 

1172 "gateway ENI for notebook internet egress (pip install, " 

1173 "git clone). No EC2 instances or Studio compute runs " 

1174 "in this subnet." 

1175 ), 

1176 ), 

1177 NagPackSuppression( 

1178 id="NIST.800.53.R5-VPCSubnetAutoAssignPublicIpDisabled", 

1179 reason=( 

1180 "The public subnet exists solely to host the NAT " 

1181 "gateway ENI. No compute workloads run here." 

1182 ), 

1183 ), 

1184 NagPackSuppression( 

1185 id="PCI.DSS.321-VPCSubnetAutoAssignPublicIpDisabled", 

1186 reason=( 

1187 "The public subnet exists solely to host the NAT " 

1188 "gateway ENI. No compute workloads run here." 

1189 ), 

1190 ), 

1191 NagPackSuppression( 

1192 id="HIPAA.Security-VPCNoUnrestrictedRouteToIGW", 

1193 reason=( 

1194 "The 0.0.0.0/0 route to the IGW is in the public " 

1195 "subnet's route table, which only hosts the NAT " 

1196 "gateway. Private subnets route through NAT, not IGW." 

1197 ), 

1198 ), 

1199 NagPackSuppression( 

1200 id="NIST.800.53.R5-VPCNoUnrestrictedRouteToIGW", 

1201 reason=( 

1202 "The 0.0.0.0/0 route to the IGW is in the public " 

1203 "subnet's route table for NAT gateway egress only." 

1204 ), 

1205 ), 

1206 NagPackSuppression( 

1207 id="PCI.DSS.321-VPCNoUnrestrictedRouteToIGW", 

1208 reason=( 

1209 "The 0.0.0.0/0 route to the IGW is in the public " 

1210 "subnet's route table for NAT gateway egress only." 

1211 ), 

1212 ), 

1213 ], 

1214 ) 

1215 

1216 

1217def add_analytics_s3_suppressions(stack: Stack) -> None: 

1218 """Add suppressions for ``Studio_Only_Bucket`` + access-logs bucket findings. 

1219 

1220 The analytics stack owns two buckets: 

1221 

1222 1. ``Studio_Only_Bucket`` — KMS-encrypted with ``Analytics_KMS_Key``, 

1223 block public access, enforce SSL, versioned. Replication is not 

1224 enabled because this bucket is the endpoint of the SageMaker 

1225 workload; cross-region replication would double storage cost and 

1226 introduce eventual-consistency behavior that breaks notebook 

1227 save/load semantics. 

1228 2. ``AnalyticsAccessLogsBucket`` — SSE-S3 encrypted because S3 

1229 server-access-log delivery to a KMS-encrypted bucket requires 

1230 additional log-delivery role plumbing that the CDK ``s3.Bucket`` 

1231 construct does not wire automatically. Replication is not enabled 

1232 because the bucket is the log sink, not a data store. 

1233 """ 

1234 NagSuppressions.add_stack_suppressions( 

1235 stack, 

1236 [ 

1237 # S3 replication suppressions — both buckets are single- 

1238 # region by design. The Studio bucket is scoped to a single 

1239 # deploy region (api-gateway region) and the access-logs 

1240 # bucket is its log sink; cross-region replication is not 

1241 # applicable to either. 

1242 NagPackSuppression( 

1243 id="HIPAA.Security-S3BucketReplicationEnabled", 

1244 reason=( 

1245 "Studio_Only_Bucket and its access-logs bucket are " 

1246 "single-region by design. The Studio bucket is the " 

1247 "endpoint of the SageMaker workload in the api-gateway " 

1248 "region; cross-region replication would double storage " 

1249 "cost without a corresponding availability gain (the " 

1250 "Studio domain itself is single-region). The access-" 

1251 "logs bucket is the log sink and is co-located with " 

1252 "the data bucket by construction." 

1253 ), 

1254 ), 

1255 NagPackSuppression( 

1256 id="NIST.800.53.R5-S3BucketReplicationEnabled", 

1257 reason=( 

1258 "Studio_Only_Bucket and its access-logs bucket are " 

1259 "single-region by design. The Studio bucket is the " 

1260 "endpoint of the SageMaker workload in the api-gateway " 

1261 "region; cross-region replication would double storage " 

1262 "cost without a corresponding availability gain (the " 

1263 "Studio domain itself is single-region). The access-" 

1264 "logs bucket is the log sink and is co-located with " 

1265 "the data bucket by construction." 

1266 ), 

1267 ), 

1268 NagPackSuppression( 

1269 id="PCI.DSS.321-S3BucketReplicationEnabled", 

1270 reason=( 

1271 "Studio_Only_Bucket and its access-logs bucket are " 

1272 "single-region by design. The Studio bucket is the " 

1273 "endpoint of the SageMaker workload in the api-gateway " 

1274 "region; cross-region replication would double storage " 

1275 "cost without a corresponding availability gain (the " 

1276 "Studio domain itself is single-region). The access-" 

1277 "logs bucket is the log sink and is co-located with " 

1278 "the data bucket by construction." 

1279 ), 

1280 ), 

1281 # Access-logs bucket KMS encryption suppressions — SSE-S3 is 

1282 # the AWS-documented pattern for server-access-log delivery 

1283 # sinks. Switching to SSE-KMS would require an additional 

1284 # log-delivery role that the CDK ``s3.Bucket`` construct does 

1285 # not wire automatically. 

1286 NagPackSuppression( 

1287 id="HIPAA.Security-S3DefaultEncryptionKMS", 

1288 reason=( 

1289 "The analytics access-logs bucket uses SSE-S3 because " 

1290 "S3 server-access-log delivery to a KMS-encrypted " 

1291 "bucket requires an additional log-delivery role " 

1292 "plumbing that CDK does not wire by default. Studio_" 

1293 "Only_Bucket (the actual data bucket) IS KMS-encrypted " 

1294 "with ``Analytics_KMS_Key``." 

1295 ), 

1296 ), 

1297 NagPackSuppression( 

1298 id="NIST.800.53.R5-S3DefaultEncryptionKMS", 

1299 reason=( 

1300 "The analytics access-logs bucket uses SSE-S3 because " 

1301 "S3 server-access-log delivery to a KMS-encrypted " 

1302 "bucket requires an additional log-delivery role " 

1303 "plumbing that CDK does not wire by default. Studio_" 

1304 "Only_Bucket (the actual data bucket) IS KMS-encrypted " 

1305 "with ``Analytics_KMS_Key``." 

1306 ), 

1307 ), 

1308 NagPackSuppression( 

1309 id="PCI.DSS.321-S3DefaultEncryptionKMS", 

1310 reason=( 

1311 "The analytics access-logs bucket uses SSE-S3 because " 

1312 "S3 server-access-log delivery to a KMS-encrypted " 

1313 "bucket requires an additional log-delivery role " 

1314 "plumbing that CDK does not wire by default. Studio_" 

1315 "Only_Bucket (the actual data bucket) IS KMS-encrypted " 

1316 "with ``Analytics_KMS_Key``." 

1317 ), 

1318 ), 

1319 ], 

1320 ) 

1321 

1322 

1323def add_presigned_url_lambda_suppressions( 

1324 stack: Stack, api_gateway_region: str | None = None 

1325) -> None: 

1326 """Add suppressions for the analytics presigned-URL Lambda role. 

1327 

1328 The Lambda needs wildcard access to SageMaker domain and user-profile 

1329 ARNs because ``CreatePresignedDomainUrl``, ``DescribeUserProfile``, 

1330 and ``CreateUserProfile`` all take ARN shapes that can only be 

1331 resolved at invoke time from the incoming Cognito username. At synth 

1332 time, ``domain/*`` and ``user-profile/*/*`` are the tightest literal 

1333 ARN shapes we can bind in the IAM policy. 

1334 """ 

1335 region = api_gateway_region or "*" 

1336 NagSuppressions.add_stack_suppressions( 

1337 stack, 

1338 [ 

1339 NagPackSuppression( 

1340 id="AwsSolutions-IAM5", 

1341 reason=( 

1342 "The presigned-URL Lambda role uses SageMaker ARN " 

1343 "wildcards on ``domain/*`` and ``user-profile/*/*`` " 

1344 "because DomainId and UserProfileName are only " 

1345 "resolvable at invoke time from the incoming Cognito " 

1346 "username. ``ListDomains`` does not support resource-" 

1347 "level scoping — the AWS API only accepts Resource::* " 

1348 "— so a ``Resource::*`` suppression is required for " 

1349 "that specific action. The effective blast radius is " 

1350 "a single paginated list call per Lambda invocation " 

1351 "against this account's SageMaker control plane in " 

1352 "the api-gateway region." 

1353 ), 

1354 applies_to=[ 

1355 "Resource::*", 

1356 (f"Resource::arn:aws:sagemaker:{region}:<AWS::AccountId>:domain/*"), 

1357 (f"Resource::arn:aws:sagemaker:{region}:<AWS::AccountId>:user-profile/*/*"), 

1358 # Generic shapes — catch tokenized-region variants 

1359 # (``<AWS::Region>``) produced when CDK synthesizes 

1360 # the policy without pinning the stack's env region. 

1361 ("Resource::arn:aws:sagemaker:<AWS::Region>:<AWS::AccountId>:domain/*"), 

1362 ("Resource::arn:aws:sagemaker:<AWS::Region>:<AWS::AccountId>:user-profile/*/*"), 

1363 ], 

1364 ), 

1365 ], 

1366 ) 

1367 

1368 

1369def add_emr_serverless_suppressions(stack: Stack) -> None: 

1370 """Add suppressions for EMR Serverless Application findings. 

1371 

1372 EMR Serverless doesn't have the same set of nag rules as EKS or Lambda; 

1373 the main structural findings relate to the application's network 

1374 configuration (which we pin to the private-isolated subnets + a 

1375 dedicated SG) and the release-label pinning (covered by a constant in 

1376 ``gco.stacks.constants``). 

1377 """ 

1378 NagSuppressions.add_stack_suppressions( 

1379 stack, 

1380 [ 

1381 # Placeholder — EMR Serverless currently has no nag rules that 

1382 # fire on a plain ``CfnApplication`` built against private 

1383 # subnets. This helper exists so the analytics branch in 

1384 # ``apply_all_suppressions`` has a single, predictable entry 

1385 # point for EMR Serverless — future EMR-related rules land 

1386 # here without touching the branch dispatch. 

1387 NagPackSuppression( 

1388 id="AwsSolutions-EMR1", 

1389 reason=( 

1390 "EMR Serverless application is created with explicit " 

1391 "private-isolated subnet ids and a dedicated security " 

1392 "group — the application never lands on public subnets." 

1393 ), 

1394 ), 

1395 ], 

1396 ) 

1397 

1398 

1399def apply_all_suppressions( 

1400 stack: Stack, 

1401 stack_type: str = "regional", 

1402 regions: list[str] | None = None, 

1403 global_region: str | None = None, 

1404 api_gateway_region: str | None = None, 

1405) -> None: 

1406 """Apply all relevant suppressions to a stack. 

1407 

1408 Args: 

1409 stack: The CDK stack to apply suppressions to 

1410 stack_type: Type of stack - 'regional', 'global', 'api_gateway', 

1411 'monitoring', or 'analytics' 

1412 regions: List of regional deployment regions (for dynamic IAM suppression patterns) 

1413 global_region: Global region for SSM parameters (for dynamic IAM suppression patterns) 

1414 api_gateway_region: API Gateway region (for analytics stack — used to 

1415 scope SageMaker execute-api and presigned-URL Lambda ARN patterns) 

1416 """ 

1417 # Common suppressions for all stacks 

1418 add_lambda_suppressions(stack) 

1419 add_iam_suppressions(stack, regions=regions, global_region=global_region) 

1420 

1421 if stack_type == "regional": 

1422 add_eks_suppressions(stack) 

1423 add_eks_cluster_suppressions(stack) 

1424 add_vpc_suppressions(stack) 

1425 add_storage_suppressions(stack) 

1426 add_sqs_suppressions(stack) 

1427 add_aurora_pgvector_suppressions(stack) 

1428 

1429 elif stack_type == "global": 

1430 add_backup_suppressions(stack) 

1431 

1432 elif stack_type == "api_gateway": 

1433 add_api_gateway_suppressions(stack) 

1434 add_secrets_suppressions(stack) 

1435 

1436 elif stack_type == "monitoring": 

1437 add_monitoring_suppressions(stack) 

1438 

1439 elif stack_type == "analytics": 

1440 # Analytics stack has S3 buckets (Studio_Only + access-logs), KMS, 

1441 # EFS, Cognito, SageMaker, EMR Serverless, and the presigned-URL 

1442 # Lambda. Each helper scopes its own applies_to list. 

1443 add_storage_suppressions(stack) 

1444 add_sagemaker_suppressions( 

1445 stack, 

1446 api_gateway_region=api_gateway_region, 

1447 global_region=global_region, 

1448 ) 

1449 add_cognito_suppressions(stack) 

1450 add_emr_serverless_suppressions(stack) 

1451 add_analytics_vpc_suppressions(stack) 

1452 add_analytics_s3_suppressions(stack) 

1453 add_presigned_url_lambda_suppressions(stack, api_gateway_region=api_gateway_region)