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
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-15 15:07 +0000
1"""CDK-nag suppression utilities for GCO stacks.
3This module provides centralized suppression management for cdk-nag rules
4that are intentionally not applicable or have documented justifications.
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
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"""
20from aws_cdk import Stack
21from cdk_nag import NagPackSuppression, NagSuppressions
22from constructs import IConstruct
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.
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.
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.
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 )
80def add_eks_suppressions(stack: Stack) -> None:
81 """Add suppressions for EKS-related cdk-nag findings.
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 ]
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 )
118def add_lambda_suppressions(stack: Stack) -> None:
119 """Add suppressions for Lambda-related cdk-nag findings.
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 ]
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 )
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.
232 CDK generates inline policies for custom resources and some patterns
233 require wildcard permissions for dynamic resource access.
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 ]
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 )
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)
268 for region in sorted(ssm_regions_set):
269 applies_to.append(f"Resource::arn:aws:ssm:{region}:<AWS::AccountId>:parameter/gco/*")
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 )
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 )
292 # KMS wildcard scoped to S3 via condition for model weights bucket decryption
293 applies_to.append("Resource::arn:aws:kms:*:<AWS::AccountId>:key/*")
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 )
339def add_vpc_suppressions(stack: Stack) -> None:
340 """Add suppressions for VPC-related cdk-nag findings.
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
708def add_aurora_pgvector_suppressions(stack: Stack) -> None:
709 """Add suppressions for Aurora pgvector-related cdk-nag findings.
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 )
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.
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.
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 "*"
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 ]
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 )
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 )
1045def add_cognito_suppressions(stack: Stack) -> None:
1046 """Add suppressions for Cognito user pool findings.
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 )
1090def add_analytics_vpc_suppressions(stack: Stack) -> None:
1091 """Add suppressions for the analytics VPC and its endpoints.
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 )
1217def add_analytics_s3_suppressions(stack: Stack) -> None:
1218 """Add suppressions for ``Studio_Only_Bucket`` + access-logs bucket findings.
1220 The analytics stack owns two buckets:
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 )
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.
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 )
1369def add_emr_serverless_suppressions(stack: Stack) -> None:
1370 """Add suppressions for EMR Serverless Application findings.
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 )
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.
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)
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)
1429 elif stack_type == "global":
1430 add_backup_suppressions(stack)
1432 elif stack_type == "api_gateway":
1433 add_api_gateway_suppressions(stack)
1434 add_secrets_suppressions(stack)
1436 elif stack_type == "monitoring":
1437 add_monitoring_suppressions(stack)
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)