Coverage for gco/stacks/nag_suppressions.py: 96.35%
142 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-30 21:22 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-30 21:22 +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 __future__ import annotations
22from collections.abc import Mapping, Sequence
23from dataclasses import dataclass, field
24from typing import Any
26from aws_cdk import (
27 IPolicyValidationPlugin,
28 Stack,
29 Token,
30 Validations,
31)
32from cdk_nag import (
33 AwsSolutionsChecks,
34 HIPAASecurityChecks,
35 NIST80053R5Checks,
36 PCIDSS321Checks,
37 ServerlessChecks,
38)
39from constructs import IConstruct
41from gco.stacks.constants import api_gateway_auth_secret_name
43# ---------------------------------------------------------------------------
44# cdk-nag v3 acknowledgment mechanism
45# ---------------------------------------------------------------------------
46# cdk-nag v3 rewrote its engine from an ``IAspect`` to an
47# ``IPolicyValidationPlugin`` (CDK's native policy-validation framework). The
48# old ``NagSuppressions.add_(resource|stack)_suppressions`` /
49# ``NagPackSuppression`` API is gone. Suppressions are now *acknowledgments*
50# recorded as construct metadata under a well-known key; cdk-nag's
51# ``isAcknowledged`` walks a construct's ancestor tree looking for that key, so
52# an acknowledgment placed on a stack (or a role) covers every matching finding
53# on that construct **and all of its descendants** — which is why there is no
54# ``apply_to_children`` flag anymore (it is always effectively ``True``).
55#
56# Finding ids come in two shapes:
57# * scalar rules -> the bare rule id, e.g. ``AwsSolutions-EC23``
58# * array rules -> ``<rule>[<detail>]``, e.g.
59# ``AwsSolutions-IAM5[Resource::*]`` — matched EXACTLY
60# (there is no bare-id fallback for these, and no regex
61# matching either: every detail must be spelled out to the
62# exact string cdk-nag emits, including any synthesis-time
63# logical-id hash such as
64# ``Resource::<RegionalSharedBucket3FF19783.Arn>/*``).
65#
66# We record acknowledgments by writing the metadata key ourselves via
67# ``node.add_metadata`` rather than calling ``Validations.of(x).acknowledge``.
68# Every detail we scope starts with ``Resource::`` / ``Policy::`` /
69# ``Action::`` / ``Condition::`` — all contain ``::``, and ``acknowledge()``
70# rejects any id containing ``::`` with ``InvalidValidationId``
71# (https://github.com/cdklabs/cdk-nag/issues/2351). Writing the metadata key
72# directly is the documented workaround and applies uniformly to every
73# suppression, scalar or array.
75# The construct-metadata key cdk-nag v3 reads for acknowledged findings.
76_ACK_METADATA_KEY: str = Validations.ACKNOWLEDGED_RULES_METADATA_KEY
79@dataclass(frozen=True)
80class NagSuppression:
81 """A single cdk-nag v3 finding acknowledgment.
83 Args:
84 id: The rule id to acknowledge (e.g. ``"AwsSolutions-IAM5"``).
85 reason: Human-readable justification recorded alongside the
86 acknowledgment.
87 applies_to: Optional finding *details* to scope the acknowledgment.
88 Each entry is matched verbatim against the ``<rule>[<detail>]``
89 finding id cdk-nag emits, so it must be the exact string —
90 including any synthesis-time logical-id hash (e.g.
91 ``Resource::<RegionalSharedBucket3FF19783.Arn>/*``). When empty,
92 the bare rule id is acknowledged (correct for scalar rules that
93 emit no ``[detail]`` suffix, such as ``AwsSolutions-EC23``).
94 """
96 id: str
97 reason: str
98 applies_to: Sequence[str] = field(default_factory=tuple)
101def _normalize(supp: NagSuppression | Mapping[str, Any]) -> tuple[str, str, list[str]]:
102 """Normalize a suppression to ``(rule_id, reason, details)``.
104 Accepts either a :class:`NagSuppression` or a plain mapping with ``id`` /
105 ``reason`` / ``applies_to`` (or the jsii spelling ``appliesTo`` used by the
106 resource-scoped call sites). Every ``applies_to`` entry must be an exact
107 string — cdk-nag v3 matches finding details verbatim (there is no regex
108 support).
109 """
110 if isinstance(supp, NagSuppression):
111 rule_id, reason, entries = supp.id, supp.reason, list(supp.applies_to)
112 else:
113 rule_id = supp["id"]
114 reason = supp["reason"]
115 entries = list(supp.get("applies_to") or supp.get("appliesTo") or [])
117 for entry in entries:
118 if not isinstance(entry, str): 118 ↛ 119line 118 didn't jump to line 119 because the condition on line 118 was never true
119 raise ValueError(
120 f"Unsupported applies_to entry for {rule_id!r}: {entry!r} "
121 "(expected an exact detail string; cdk-nag v3 has no regex support)"
122 )
123 return rule_id, reason, entries
126def acknowledge_nag_findings(
127 scope: IConstruct,
128 suppressions: Sequence[NagSuppression | Mapping[str, Any]],
129) -> None:
130 """Record cdk-nag v3 acknowledgments on ``scope`` (covers its descendants).
132 This is GCO's v3-native replacement for the removed
133 ``NagSuppressions.add_resource_suppressions`` /
134 ``add_stack_suppressions``. Each finding id (``<rule>[<detail>]`` for
135 scoped details, or the bare ``<rule>`` when ``applies_to`` is empty) is
136 written to the cdk-nag acknowledgment metadata key, which every rule pack
137 honors natively. Because cdk-nag walks the ancestor tree, an
138 acknowledgment on a stack covers all resources in that stack, and one on a
139 role covers the role's generated policies.
140 """
141 # cdk-nag renders the CloudFormation partition/account/region
142 # pseudo-parameters in a finding's detail two different ways, and which one
143 # appears is decided per ARN, not per stack:
144 # * an ARN built from ``Aws.PARTITION`` / ``Aws.ACCOUNT_ID`` /
145 # ``Aws.REGION`` may retain the angle-bracket pseudo-param literal,
146 # whereas
147 # * an ARN hand-built from ``stack.partition`` / ``stack.account`` /
148 # ``stack.region`` renders concrete values when CDK can resolve them.
149 # So for a concrete env we can't know from here which form cdk-nag will emit
150 # for any given finding. Register the acknowledgment under both the
151 # placeholder detail and every resolvable literal rendering. Extra keys
152 # that match no finding are harmless; whichever exact form cdk-nag emits
153 # then has a matching acknowledgment. Any detail built from a raw token is
154 # normalized first because unresolved tokens cannot be metadata-map keys.
155 stack = Stack.of(scope)
156 dimensions = (
157 (stack.partition, "<AWS::Partition>"),
158 (stack.account, "<AWS::AccountId>"),
159 (stack.region, "<AWS::Region>"),
160 )
162 def _keys_for(rule_id: str, detail: str) -> list[str]:
163 for value, placeholder in dimensions:
164 if Token.is_unresolved(value):
165 detail = detail.replace(value, placeholder)
166 if Token.is_unresolved(detail): 166 ↛ 167line 166 didn't jump to line 167 because the condition on line 166 was never true
167 raise ValueError(
168 "cdk-nag acknowledgment detail contains an unresolved token and "
169 f"cannot be used as a metadata key: {detail!r}. Use cdk-nag's "
170 "literal rendering (e.g. '<AWS::Region>', '<LogicalId.Arn>') instead."
171 )
172 # Expand each placeholder into every concrete environment dimension
173 # CDK resolved, producing the exact partition/account/region combinations
174 # cdk-nag can render for the same policy resource.
175 variants = {detail}
176 for value, placeholder in dimensions:
177 if not Token.is_unresolved(value):
178 variants.update(v.replace(placeholder, value) for v in list(variants))
179 return [f"{rule_id}[{v}]" for v in variants]
181 ack: dict[str, str] = {}
182 for supp in suppressions:
183 rule_id, reason, details = _normalize(supp)
184 if not details:
185 ack[rule_id] = reason
186 for detail in details:
187 for key in _keys_for(rule_id, detail):
188 ack[key] = reason
189 if ack: 189 ↛ exitline 189 didn't return from function 'acknowledge_nag_findings' because the condition on line 189 was always true
190 scope.node.add_metadata(_ACK_METADATA_KEY, ack)
193# The cdk-nag rules that evaluate security-group *ingress CIDRs*. When an
194# ingress rule's CIDR is a CloudFormation token (e.g. a VPC CIDR resolved via
195# ``Fn::GetAtt``), these rules cannot resolve it to a primitive value and
196# *throw*. cdk-nag v3 surfaces a thrown rule under its bare id (the v2
197# ``CdkNagValidationFailure`` aggregate rule is gone), so a single unresolvable
198# ingress rule produces one bare finding per rule below. Every GCO security
199# group whose ingress is pinned to its own VPC CIDR trips this exact set.
200_SECURITY_GROUP_CIDR_RULES: tuple[str, ...] = (
201 "AwsSolutions-EC23",
202 "HIPAA.Security-EC2RestrictedCommonPorts",
203 "HIPAA.Security-EC2RestrictedSSH",
204 "NIST.800.53.R5-EC2RestrictedCommonPorts",
205 "NIST.800.53.R5-EC2RestrictedSSH",
206 "PCI.DSS.321-EC2RestrictedCommonPorts",
207 "PCI.DSS.321-EC2RestrictedSSH",
208)
211def acknowledge_security_group_cidr_findings(scope: IConstruct, *, reason: str) -> None:
212 """Acknowledge the SG-ingress rules that throw on a token (VPC CIDR) source.
214 Scope this to the specific security-group-bearing construct — an EKS
215 cluster, a VPC whose interface endpoints carry security groups, or a
216 standalone ``SecurityGroup`` — rather than the whole stack, so the bare-id
217 acknowledgment cannot mask a genuine open-ingress finding elsewhere.
218 cdk-nag walks the ancestor tree, so the acknowledgment covers every ingress
219 rule under ``scope``.
220 """
221 acknowledge_nag_findings(
222 scope,
223 [NagSuppression(id=rule, reason=reason) for rule in _SECURITY_GROUP_CIDR_RULES],
224 )
227def nag_validation_plugins(
228 scope: IConstruct, *, verbose: bool = True
229) -> list[IPolicyValidationPlugin]:
230 """Return the five GCO cdk-nag rule packs as v3 policy-validation plugins.
232 Register with ``Validations.of(app).add_plugins(*nag_validation_plugins(app))``.
233 Each pack reads the acknowledgment metadata written by
234 :func:`acknowledge_nag_findings` natively, so the packs run directly with
235 no wrapping.
236 """
237 return [
238 AwsSolutionsChecks(scope, verbose=verbose),
239 HIPAASecurityChecks(scope, verbose=verbose),
240 NIST80053R5Checks(scope, verbose=verbose),
241 PCIDSS321Checks(scope, verbose=verbose),
242 ServerlessChecks(scope, verbose=verbose),
243 ]
246def suppress_managed_policy_opt_in(
247 resource: IConstruct,
248 *,
249 managed_policy_name: str,
250 reason: str,
251) -> None:
252 """Scoped ``AwsSolutions-IAM4`` suppression for an intentional managed-policy attach.
254 The house pattern for GCO is to enumerate least-privilege
255 statements rather than attach AWS-managed policies, but a handful
256 of opt-in sub-features (e.g. SageMaker Canvas) *must* track a
257 managed policy because the underlying service's per-feature
258 permission surface evolves faster than we can keep up with. For
259 those cases we accept the ``AwsSolutions-IAM4`` finding with a
260 scoped suppression rather than a broad one.
262 This helper is the single call-site format for that pattern: pass
263 the resource, the managed-policy name, and a one-line reason
264 describing why the policy is appropriate for your feature. The
265 helper expands the standard ``Policy::arn:<AWS::Partition>:iam::
266 aws:policy/<name>`` applies-to ARN so every managed-policy opt-in
267 in the codebase uses the same suppression shape — reviewers can
268 grep for ``suppress_managed_policy_opt_in(`` to find every one.
270 Args:
271 resource: The IAM role (or other CDK construct) receiving the
272 managed-policy attachment.
273 managed_policy_name: The bare managed-policy name (e.g.
274 ``"AmazonSageMakerCanvasFullAccess"``). Must NOT include
275 the ``arn:<partition>:iam::aws:policy/`` prefix — the
276 helper adds that.
277 reason: Human-readable justification. Must explain (a) why
278 the managed policy is preferred over an enumerated
279 least-privilege policy, and (b) what the toggle or
280 conditional is that gates the attachment (so reviewers
281 can confirm the wider permission surface is opt-in).
282 """
283 acknowledge_nag_findings(
284 resource,
285 [
286 NagSuppression(
287 id="AwsSolutions-IAM4",
288 reason=reason,
289 applies_to=[
290 f"Policy::arn:<AWS::Partition>:iam::aws:policy/{managed_policy_name}",
291 ],
292 ),
293 ],
294 )
297def add_eks_suppressions(stack: Stack) -> None:
298 """Add suppressions for EKS-related cdk-nag findings.
300 EKS requires specific AWS managed policies that cannot be replaced
301 with customer-managed policies without breaking functionality.
302 """
303 # EKS requires these AWS managed policies - they are AWS-recommended
304 eks_managed_policies = [
305 "Policy::arn:<AWS::Partition>:iam::aws:policy/AmazonEKSClusterPolicy",
306 "Policy::arn:<AWS::Partition>:iam::aws:policy/AmazonEKSComputePolicy",
307 "Policy::arn:<AWS::Partition>:iam::aws:policy/AmazonEKSBlockStoragePolicy",
308 "Policy::arn:<AWS::Partition>:iam::aws:policy/AmazonEKSLoadBalancingPolicy",
309 "Policy::arn:<AWS::Partition>:iam::aws:policy/AmazonEKSNetworkingPolicy",
310 "Policy::arn:<AWS::Partition>:iam::aws:policy/AmazonEKSWorkerNodePolicy",
311 "Policy::arn:<AWS::Partition>:iam::aws:policy/AmazonEKS_CNI_Policy",
312 "Policy::arn:<AWS::Partition>:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly",
313 "Policy::arn:<AWS::Partition>:iam::aws:policy/service-role/AmazonEFSCSIDriverPolicy",
314 # CloudWatch Observability addon policies for Container Insights
315 "Policy::arn:<AWS::Partition>:iam::aws:policy/CloudWatchAgentServerPolicy",
316 "Policy::arn:<AWS::Partition>:iam::aws:policy/AWSXrayWriteOnlyAccess",
317 ]
319 acknowledge_nag_findings(
320 stack,
321 [
322 NagSuppression(
323 id="AwsSolutions-IAM4",
324 reason=(
325 "EKS requires AWS managed policies for cluster, node, and add-on functionality. "
326 "These are AWS-recommended policies that provide necessary permissions for EKS Auto Mode. "
327 "See: https://docs.aws.amazon.com/eks/latest/userguide/security-iam-awsmanpol.html"
328 ),
329 applies_to=eks_managed_policies,
330 ),
331 ],
332 )
335def add_lambda_suppressions(stack: Stack) -> None:
336 """Add suppressions for Lambda-related cdk-nag findings.
338 Lambda functions used for CDK custom resources and infrastructure
339 automation have specific requirements that trigger cdk-nag warnings.
340 """
341 lambda_managed_policies = [
342 "Policy::arn:<AWS::Partition>:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole",
343 "Policy::arn:<AWS::Partition>:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole",
344 ]
346 acknowledge_nag_findings(
347 stack,
348 [
349 NagSuppression(
350 id="AwsSolutions-IAM4",
351 reason=(
352 "Lambda basic execution and VPC access roles are AWS-recommended managed policies. "
353 "They provide minimal permissions for CloudWatch Logs and VPC ENI management. "
354 "See: https://docs.aws.amazon.com/lambda/latest/dg/lambda-intro-execution-role.html"
355 ),
356 applies_to=lambda_managed_policies,
357 ),
358 NagSuppression(
359 id="AwsSolutions-L1",
360 reason=(
361 "CDK Provider framework Lambda functions use a specific runtime version "
362 "managed by CDK. These are internal functions not exposed to users."
363 ),
364 ),
365 # HIPAA Lambda suppressions
366 NagSuppression(
367 id="HIPAA.Security-LambdaConcurrency",
368 reason=(
369 "Infrastructure Lambda functions (custom resources) are invoked only during "
370 "stack deployment and do not require concurrency limits. They are not user-facing."
371 ),
372 ),
373 NagSuppression(
374 id="HIPAA.Security-LambdaDLQ",
375 reason=(
376 "CDK custom resource Lambda functions have built-in retry logic and report "
377 "failures directly to CloudFormation. DLQ is not applicable for this pattern."
378 ),
379 ),
380 NagSuppression(
381 id="HIPAA.Security-LambdaInsideVPC",
382 reason=(
383 "CDK Provider framework Lambda functions need internet access to communicate "
384 "with CloudFormation. VPC placement would require NAT Gateway configuration. "
385 "User-facing Lambda functions (kubectl applier) ARE placed in VPC."
386 ),
387 ),
388 # NIST 800-53 Lambda suppressions
389 NagSuppression(
390 id="NIST.800.53.R5-LambdaConcurrency",
391 reason=(
392 "Infrastructure Lambda functions (custom resources) are invoked only during "
393 "stack deployment and do not require concurrency limits."
394 ),
395 ),
396 NagSuppression(
397 id="NIST.800.53.R5-LambdaDLQ",
398 reason=(
399 "CDK custom resource Lambda functions have built-in retry logic and report "
400 "failures directly to CloudFormation. DLQ is not applicable."
401 ),
402 ),
403 NagSuppression(
404 id="NIST.800.53.R5-LambdaInsideVPC",
405 reason=(
406 "CDK Provider framework Lambda functions need internet access to communicate "
407 "with CloudFormation. User-facing Lambda functions ARE placed in VPC."
408 ),
409 ),
410 # PCI DSS Lambda suppressions
411 NagSuppression(
412 id="PCI.DSS.321-LambdaInsideVPC",
413 reason=(
414 "CDK Provider framework Lambda functions need internet access to communicate "
415 "with CloudFormation. User-facing Lambda functions ARE placed in VPC."
416 ),
417 ),
418 # Serverless Lambda suppressions
419 NagSuppression(
420 id="Serverless-LambdaLatestVersion",
421 reason=(
422 "CDK Provider framework Lambda functions use a specific runtime version "
423 "managed by CDK. These are internal functions not exposed to users."
424 ),
425 ),
426 NagSuppression(
427 id="Serverless-LambdaDefaultMemorySize",
428 reason=(
429 "CDK Provider framework Lambda functions have appropriate memory for their "
430 "workload. Custom Lambda functions have explicit memory configuration."
431 ),
432 ),
433 NagSuppression(
434 id="Serverless-LambdaDLQ",
435 reason=(
436 "CDK custom resource Lambda functions have built-in retry logic and report "
437 "failures directly to CloudFormation. DLQ is not applicable."
438 ),
439 ),
440 ],
441 )
444def add_iam_suppressions(
445 stack: Stack,
446 regions: list[str] | None = None,
447 global_region: str | None = None,
448 api_gateway_region: str | None = None,
449 project_name: str = "gco",
450) -> None:
451 """Add suppressions for IAM-related cdk-nag findings.
453 CDK generates inline policies for custom resources and some patterns
454 require wildcard permissions for dynamic resource access.
456 Args:
457 stack: The CDK stack to apply suppressions to
458 regions: List of regional deployment regions (for EKS addon patterns)
459 global_region: Global region for SSM parameters and DynamoDB tables
460 api_gateway_region: Region where the API Gateway auth secret lives.
461 The regional service-account role's read grant is scoped to a
462 deterministic ARN in this region (see ``GCORegionalStack`` and
463 issue #125). Falls back to ``global_region`` for callers that
464 don't split the API Gateway stack into its own region — the two
465 are co-located in the default topology.
466 project_name: Deployment prefix (#139). Every allow-list ARN pattern
467 below (auth secret, ``/<project>/`` SSM tree, ``<project>-*``
468 DynamoDB tables and S3 buckets) is scoped to this so the
469 suppression matches the deployment's actual, project-scoped
470 resource names. Defaults to ``"gco"`` so the rendered patterns are
471 byte-for-byte identical to the pre-#139 literals for the stock
472 deployment.
473 """
474 # Region the API Gateway auth secret is created in. It is normally the
475 # same as ``global_region`` (default cdk.json co-locates them), but the
476 # secret physically lives in the API Gateway stack's region, so scope the
477 # grant/suppression to that region when it is provided.
478 secret_region = api_gateway_region or global_region or "us-east-2"
480 # Build dynamic applies_to list based on configured regions
481 applies_to = [
482 # The convergence Step Functions state machine's role invokes each
483 # Lambda task's versions — CDK's LambdaInvoke grants `<fn>.Arn:*` for
484 # the version/alias qualifier, which is what these `:*` findings flag.
485 # kubectl-applier: the base and post-Helm manifest passes.
486 "Resource::<KubectlApplierFunction6147DA0C.Arn>:*",
487 # GA-registration: the final Global Accelerator registration task.
488 "Resource::<GaRegistrationFunction4A12C41B.Arn>:*",
489 # Delete-time GA deregistration guard (issue #130): its cr.Provider
490 # framework-onEvent role invokes the deregistration Lambda's versions.
491 "Resource::<GaDeregistrationFunction5CFAADA4.Arn>:*",
492 # helm-installer: one task per Helm chart.
493 "Resource::<HelmInstallerFunction3FEB04EF.Arn>:*",
494 # VPC Flow Logs delivery role writes log events to every stream in the
495 # flow-log group (logs:CreateLogStream/PutLogEvents on `<group>.Arn:*`).
496 "Resource::<VpcFlowLogGroup86559C69.Arn>:*",
497 # Secrets Manager access for the backend HMAC signing key, with a
498 # trailing wildcard for the random 6-char suffix. The regional
499 # service-account role builds this exact ARN deterministically (from
500 # the secret name + API Gateway region + account) so it matches in
501 # both single-region and cross-region topologies — see issue #125.
502 f"Resource::arn:<AWS::Partition>:secretsmanager:{secret_region}:<AWS::AccountId>:secret:{api_gateway_auth_secret_name(project_name)}*",
503 ]
505 # Add EKS addon patterns for each configured region
506 if regions:
507 for region in regions:
508 applies_to.append(
509 f"Resource::arn:<AWS::Partition>:eks:{region}:<AWS::AccountId>:addon/<GCOEksCluster841A896A>/*"
510 )
512 # Add SSM parameter patterns for global region and all regional regions.
513 # Using ``dict.fromkeys`` (insertion-ordered) + sorting gives a stable
514 # ordering so the cdk-nag metadata block doesn't churn between synths
515 # when PYTHONHASHSEED changes — previous ``set()`` iteration order was
516 # hash-based and produced non-deterministic template diffs.
517 ssm_regions_set: set[str] = set()
518 if global_region:
519 ssm_regions_set.add(global_region)
520 if regions:
521 ssm_regions_set.update(regions)
523 for region in sorted(ssm_regions_set):
524 applies_to.append(
525 f"Resource::arn:<AWS::Partition>:ssm:{region}:<AWS::AccountId>:parameter/{project_name}/*"
526 )
527 # Per-chart add-on status + replay input written by the helm installer
528 # and orchestrator (gco stacks addons status/install). Scoped to the
529 # project's addons subtree in each region.
530 applies_to.append(
531 f"Resource::arn:<AWS::Partition>:ssm:{region}:<AWS::AccountId>:parameter/{project_name}/addons/*"
532 )
534 # Add DynamoDB index wildcard patterns for global region
535 # Tables are created in global stack, accessed from all regional stacks
536 if global_region:
537 applies_to.extend(
538 [
539 f"Resource::arn:<AWS::Partition>:dynamodb:{global_region}:<AWS::AccountId>:table/{project_name}-job-templates/index/*",
540 f"Resource::arn:<AWS::Partition>:dynamodb:{global_region}:<AWS::AccountId>:table/{project_name}-webhooks/index/*",
541 f"Resource::arn:<AWS::Partition>:dynamodb:{global_region}:<AWS::AccountId>:table/{project_name}-jobs/index/*",
542 f"Resource::arn:<AWS::Partition>:dynamodb:{global_region}:<AWS::AccountId>:table/{project_name}-inference-endpoints/index/*",
543 ]
544 )
546 # Add S3 wildcard patterns for the project's buckets. CDK auto-generates
547 # some bucket names from the stack name (``<project_name>-<region>``), and
548 # the always-on buckets use explicit ``<project_name>-...`` names, so a
549 # single ``<project_name>-*`` prefix covers both.
550 applies_to.extend(
551 [
552 f"Resource::arn:<AWS::Partition>:s3:::{project_name}-*",
553 f"Resource::arn:<AWS::Partition>:s3:::{project_name}-*/*",
554 ]
555 )
557 # KMS wildcard scoped to S3 via condition for model weights bucket decryption
558 applies_to.append("Resource::arn:<AWS::Partition>:kms:*:<AWS::AccountId>:key/*")
560 acknowledge_nag_findings(
561 stack,
562 [
563 # Inline policy suppressions for all frameworks
564 NagSuppression(
565 id="HIPAA.Security-IAMNoInlinePolicy",
566 reason=(
567 "CDK generates inline policies for custom resources and Lambda functions. "
568 "These are scoped to specific resources and follow least-privilege principles."
569 ),
570 ),
571 NagSuppression(
572 id="NIST.800.53.R5-IAMNoInlinePolicy",
573 reason=(
574 "CDK generates inline policies for custom resources and Lambda functions. "
575 "These are scoped to specific resources and follow least-privilege principles."
576 ),
577 ),
578 NagSuppression(
579 id="PCI.DSS.321-IAMNoInlinePolicy",
580 reason=(
581 "CDK generates inline policies for custom resources and Lambda functions. "
582 "These are scoped to specific resources and follow least-privilege principles."
583 ),
584 ),
585 # Wildcard permission suppressions
586 NagSuppression(
587 id="AwsSolutions-IAM5",
588 reason=(
589 "Wildcard permissions are required for: (1) EKS cluster admin access to manage "
590 "dynamic Kubernetes resources, (2) Custom resource providers to invoke Lambda versions, "
591 "(3) SSM parameter access for cross-region coordination, (4) EKS addon management, "
592 "(5) VPC Flow Logs to write to CloudWatch, (6) Secrets Manager cross-region access "
593 "with wildcard suffix for the HMAC signing key, (7) DynamoDB GSI access for job queue, templates, "
594 "webhooks, and inference endpoints tables, (8) S3 access for model weights bucket "
595 "(auto-generated name). All wildcards are scoped to specific patterns. "
596 "(9) KMS decrypt scoped to S3 via condition for model weights bucket."
597 ),
598 applies_to=applies_to,
599 ),
600 ],
601 )
604def add_vpc_suppressions(stack: Stack) -> None:
605 """Add suppressions for VPC-related cdk-nag findings.
607 Public subnets and IGW routes host the NAT gateways that provide bounded
608 egress for private EKS nodes and VPC Lambdas. The platform ALB remains internal.
609 """
610 acknowledge_nag_findings(
611 stack,
612 [
613 # HIPAA VPC suppressions
614 NagSuppression(
615 id="HIPAA.Security-VPCSubnetAutoAssignPublicIpDisabled",
616 reason=(
617 "Public subnets host NAT gateways that provide controlled egress "
618 "for private EKS nodes and VPC Lambdas; workloads and the platform "
619 "ALB remain in private subnets."
620 ),
621 ),
622 NagSuppression(
623 id="HIPAA.Security-VPCNoUnrestrictedRouteToIGW",
624 reason=(
625 "Public-subnet NAT gateways require an Internet Gateway route to "
626 "provide outbound dependency access for private subnets. EKS nodes, "
627 "VPC Lambdas, and the platform ALB remain private."
628 ),
629 ),
630 # NIST 800-53 VPC suppressions
631 NagSuppression(
632 id="NIST.800.53.R5-VPCSubnetAutoAssignPublicIpDisabled",
633 reason=(
634 "Public subnets host NAT gateways that provide controlled egress "
635 "for private EKS nodes and VPC Lambdas; workloads and the platform "
636 "ALB remain in private subnets."
637 ),
638 ),
639 NagSuppression(
640 id="NIST.800.53.R5-VPCNoUnrestrictedRouteToIGW",
641 reason=(
642 "Public-subnet NAT gateways require an Internet Gateway route to "
643 "provide outbound dependency access for private subnets. EKS nodes, "
644 "VPC Lambdas, and the platform ALB remain private."
645 ),
646 ),
647 # PCI DSS VPC suppressions
648 NagSuppression(
649 id="PCI.DSS.321-VPCSubnetAutoAssignPublicIpDisabled",
650 reason=(
651 "Public subnets host NAT gateways that provide controlled egress "
652 "for private EKS nodes and VPC Lambdas; workloads and the platform "
653 "ALB remain in private subnets."
654 ),
655 ),
656 NagSuppression(
657 id="PCI.DSS.321-VPCNoUnrestrictedRouteToIGW",
658 reason=(
659 "Public-subnet NAT gateways require an Internet Gateway route to "
660 "provide outbound dependency access for private subnets. EKS nodes, "
661 "VPC Lambdas, and the platform ALB remain private."
662 ),
663 ),
664 ],
665 )
668def add_api_gateway_suppressions(stack: Stack) -> None:
669 """Add suppressions for API Gateway-related cdk-nag findings."""
670 acknowledge_nag_findings(
671 stack,
672 [
673 NagSuppression(
674 id="AwsSolutions-COG4",
675 reason=(
676 "API Gateway uses IAM authentication (SigV4) instead of Cognito. "
677 "This is intentional for machine-to-machine API access patterns."
678 ),
679 ),
680 NagSuppression(
681 id="AwsSolutions-APIG2",
682 reason=(
683 "Request validation is performed by the backend Manifest Processor service "
684 "which has detailed schema validation. API Gateway acts as a pass-through proxy."
685 ),
686 ),
687 # Cache suppressions - caching is intentionally disabled
688 NagSuppression(
689 id="HIPAA.Security-APIGWCacheEnabledAndEncrypted",
690 reason=(
691 "Caching is disabled intentionally. Manifest submissions are unique "
692 "and should not be cached. Health checks need real-time data."
693 ),
694 ),
695 NagSuppression(
696 id="NIST.800.53.R5-APIGWCacheEnabledAndEncrypted",
697 reason=(
698 "Caching is disabled intentionally. Manifest submissions are unique "
699 "and should not be cached. Health checks need real-time data."
700 ),
701 ),
702 NagSuppression(
703 id="PCI.DSS.321-APIGWCacheEnabledAndEncrypted",
704 reason=(
705 "Caching is disabled intentionally. Manifest submissions are unique "
706 "and should not be cached. Health checks need real-time data."
707 ),
708 ),
709 # SSL certificate suppressions
710 NagSuppression(
711 id="HIPAA.Security-APIGWSSLEnabled",
712 reason=(
713 "API Gateway integrates with Lambda rather than an HTTP backend that "
714 "requires an API Gateway client certificate. The public API endpoint "
715 "enforces HTTPS; Lambda signs the exact private-backend request with "
716 "a short-lived HMAC envelope."
717 ),
718 ),
719 NagSuppression(
720 id="NIST.800.53.R5-APIGWSSLEnabled",
721 reason=(
722 "API Gateway integrates with Lambda rather than an HTTP backend that "
723 "requires an API Gateway client certificate. The public API endpoint "
724 "enforces HTTPS; Lambda signs the exact private-backend request with "
725 "a short-lived HMAC envelope."
726 ),
727 ),
728 NagSuppression(
729 id="PCI.DSS.321-APIGWSSLEnabled",
730 reason=(
731 "API Gateway integrates with Lambda rather than an HTTP backend that "
732 "requires an API Gateway client certificate. The public API endpoint "
733 "enforces HTTPS; Lambda signs the exact private-backend request with "
734 "a short-lived HMAC envelope."
735 ),
736 ),
737 # CloudWatch Log Group encryption suppressions
738 NagSuppression(
739 id="HIPAA.Security-CloudWatchLogGroupEncrypted",
740 reason=(
741 "CloudWatch Logs are encrypted by default with AWS-managed keys. "
742 "Customer-managed KMS keys can be enabled via configuration if required."
743 ),
744 ),
745 NagSuppression(
746 id="NIST.800.53.R5-CloudWatchLogGroupEncrypted",
747 reason=(
748 "CloudWatch Logs are encrypted by default with AWS-managed keys. "
749 "Customer-managed KMS keys can be enabled via configuration if required."
750 ),
751 ),
752 NagSuppression(
753 id="PCI.DSS.321-CloudWatchLogGroupEncrypted",
754 reason=(
755 "CloudWatch Logs are encrypted by default with AWS-managed keys. "
756 "Customer-managed KMS keys can be enabled via configuration if required."
757 ),
758 ),
759 # API Gateway CloudWatch role
760 NagSuppression(
761 id="AwsSolutions-IAM4",
762 reason=(
763 "API Gateway CloudWatch role requires the AWS managed policy "
764 "AmazonAPIGatewayPushToCloudWatchLogs for logging functionality."
765 ),
766 applies_to=[
767 "Policy::arn:<AWS::Partition>:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs",
768 ],
769 ),
770 ],
771 )
774def add_monitoring_suppressions(stack: Stack) -> None:
775 """Add suppressions for monitoring-related cdk-nag findings."""
776 acknowledge_nag_findings(
777 stack,
778 [
779 NagSuppression(
780 id="AwsSolutions-SNS3",
781 reason="SNS topic has enforce_ssl=True enabled, which adds the required policy.",
782 ),
783 NagSuppression(
784 id="HIPAA.Security-SNSEncryptedKMS",
785 reason=(
786 "Alert notifications contain operational data (alarm names, thresholds) "
787 "not PHI. KMS encryption adds latency to time-sensitive alerts."
788 ),
789 ),
790 NagSuppression(
791 id="NIST.800.53.R5-SNSEncryptedKMS",
792 reason=(
793 "Alert notifications contain operational data (alarm names, thresholds). "
794 "KMS encryption adds latency to time-sensitive alerts."
795 ),
796 ),
797 NagSuppression(
798 id="PCI.DSS.321-SNSEncryptedKMS",
799 reason=(
800 "Alert notifications contain operational data (alarm names, thresholds). "
801 "KMS encryption can be enabled if required for PCI compliance."
802 ),
803 ),
804 # CloudWatch Log Group encryption
805 NagSuppression(
806 id="HIPAA.Security-CloudWatchLogGroupEncrypted",
807 reason="CloudWatch Logs are encrypted by default with AWS-managed keys.",
808 ),
809 NagSuppression(
810 id="NIST.800.53.R5-CloudWatchLogGroupEncrypted",
811 reason="CloudWatch Logs are encrypted by default with AWS-managed keys.",
812 ),
813 NagSuppression(
814 id="PCI.DSS.321-CloudWatchLogGroupEncrypted",
815 reason="CloudWatch Logs are encrypted by default with AWS-managed keys.",
816 ),
817 # CloudWatch Alarm Action suppressions for composite alarm inputs
818 # These alarms are intentionally used only as inputs to composite alarms
819 # The composite alarms have actions attached, not the individual alarms
820 NagSuppression(
821 id="HIPAA.Security-CloudWatchAlarmAction",
822 reason=(
823 "These alarms are inputs to composite alarms which have SNS actions. "
824 "Individual alarms don't need actions as they're aggregated for better signal-to-noise."
825 ),
826 ),
827 NagSuppression(
828 id="NIST.800.53.R5-CloudWatchAlarmAction",
829 reason=(
830 "These alarms are inputs to composite alarms which have SNS actions. "
831 "Individual alarms don't need actions as they're aggregated for better signal-to-noise."
832 ),
833 ),
834 ],
835 )
838def add_storage_suppressions(stack: Stack) -> None:
839 """Add suppressions for storage-related cdk-nag findings."""
840 acknowledge_nag_findings(
841 stack,
842 [
843 # EFS backup suppressions
844 NagSuppression(
845 id="HIPAA.Security-EFSInBackupPlan",
846 reason=(
847 "EFS backup is optional and can be enabled via AWS Backup if required. "
848 "Default deployment prioritizes cost optimization."
849 ),
850 ),
851 NagSuppression(
852 id="NIST.800.53.R5-EFSInBackupPlan",
853 reason=(
854 "EFS backup is optional and can be enabled via AWS Backup if required. "
855 "Default deployment prioritizes cost optimization."
856 ),
857 ),
858 # CloudWatch Log Group encryption
859 NagSuppression(
860 id="HIPAA.Security-CloudWatchLogGroupEncrypted",
861 reason=(
862 "CloudWatch Logs are encrypted by default with AWS-managed keys. "
863 "CDK Provider log groups are for infrastructure automation only."
864 ),
865 ),
866 NagSuppression(
867 id="NIST.800.53.R5-CloudWatchLogGroupEncrypted",
868 reason=(
869 "CloudWatch Logs are encrypted by default with AWS-managed keys. "
870 "CDK Provider log groups are for infrastructure automation only."
871 ),
872 ),
873 NagSuppression(
874 id="PCI.DSS.321-CloudWatchLogGroupEncrypted",
875 reason=(
876 "CloudWatch Logs are encrypted by default with AWS-managed keys. "
877 "CDK Provider log groups are for infrastructure automation only."
878 ),
879 ),
880 ],
881 )
884def add_sqs_suppressions(stack: Stack) -> None:
885 """Add suppressions for SQS-related cdk-nag findings."""
886 acknowledge_nag_findings(
887 stack,
888 [
889 NagSuppression(
890 id="AwsSolutions-SQS4",
891 reason="SQS queues have enforce_ssl=True enabled, which adds the required policy.",
892 ),
893 NagSuppression(
894 id="Serverless-SQSRedrivePolicy",
895 reason=(
896 "The dead-letter queue itself does not need a redrive policy. "
897 "The main job queue has a redrive policy pointing to the DLQ."
898 ),
899 ),
900 ],
901 )
904def add_secrets_suppressions(stack: Stack) -> None:
905 """Add suppressions for Secrets Manager-related cdk-nag findings."""
906 acknowledge_nag_findings(
907 stack,
908 [
909 # KMS key suppressions - using AWS-managed keys is acceptable
910 NagSuppression(
911 id="HIPAA.Security-SecretsManagerUsingKMSKey",
912 reason=(
913 "Secrets Manager encrypts secrets by default with AWS-managed keys. "
914 "Customer-managed KMS can be enabled if required for compliance."
915 ),
916 ),
917 NagSuppression(
918 id="NIST.800.53.R5-SecretsManagerUsingKMSKey",
919 reason="Secrets Manager encrypts secrets by default with AWS-managed keys.",
920 ),
921 NagSuppression(
922 id="PCI.DSS.321-SecretsManagerUsingKMSKey",
923 reason=(
924 "Secrets Manager encrypts secrets by default with AWS-managed keys. "
925 "Customer-managed KMS can be enabled if required for PCI compliance."
926 ),
927 ),
928 ],
929 )
932def add_eks_cluster_suppressions(stack: Stack) -> None:
933 """Add suppressions for EKS cluster-specific findings."""
934 acknowledge_nag_findings(
935 stack,
936 [
937 NagSuppression(
938 id="AwsSolutions-EKS1",
939 reason=(
940 "The production default is a private EKS API endpoint. When operators "
941 "explicitly opt into PUBLIC_AND_PRIVATE for development access, the "
942 "public endpoint is authenticated and authorized through IAM."
943 ),
944 ),
945 ],
946 )
949def add_backup_suppressions(stack: Stack) -> None:
950 """Add suppressions for AWS Backup-related cdk-nag findings."""
951 acknowledge_nag_findings(
952 stack,
953 [
954 NagSuppression(
955 id="AwsSolutions-IAM4",
956 reason=(
957 "AWS Backup requires the AWSBackupServiceRolePolicyForBackup managed policy "
958 "attached to the backup service role to perform backup operations on DynamoDB tables. "
959 "This is the AWS-recommended policy for AWS Backup default service roles. "
960 "See: https://docs.aws.amazon.com/aws-backup/latest/devguide/iam-service-roles.html"
961 ),
962 applies_to=[
963 "Policy::arn:<AWS::Partition>:iam::aws:policy/service-role/AWSBackupServiceRolePolicyForBackup",
964 ],
965 ),
966 ],
967 )
970def add_aurora_pgvector_suppressions(stack: Stack) -> None:
971 """Add suppressions for Aurora pgvector-related cdk-nag findings.
973 Aurora Serverless v2 with pgvector triggers several compliance findings
974 that are intentionally accepted for this deployment pattern.
975 """
976 acknowledge_nag_findings(
977 stack,
978 [
979 # Secrets Manager KMS key — Aurora secret uses AWS-managed encryption
980 NagSuppression(
981 id="HIPAA.Security-SecretsManagerUsingKMSKey",
982 reason=(
983 "Aurora Serverless v2 credentials in Secrets Manager are encrypted with "
984 "AWS-managed keys by default. Customer-managed KMS can be enabled if required."
985 ),
986 ),
987 NagSuppression(
988 id="NIST.800.53.R5-SecretsManagerUsingKMSKey",
989 reason=(
990 "Aurora Serverless v2 credentials in Secrets Manager are encrypted with "
991 "AWS-managed keys by default."
992 ),
993 ),
994 # Secrets Manager rotation — Aurora manages rotation via RDS integration
995 NagSuppression(
996 id="HIPAA.Security-SecretsManagerRotationEnabled",
997 reason=(
998 "Aurora manages credential rotation via the RDS integration with Secrets "
999 "Manager. Manual rotation configuration is not required."
1000 ),
1001 ),
1002 NagSuppression(
1003 id="NIST.800.53.R5-SecretsManagerRotationEnabled",
1004 reason=(
1005 "Aurora manages credential rotation via the RDS integration with Secrets "
1006 "Manager. Manual rotation configuration is not required."
1007 ),
1008 ),
1009 # RDS in backup plan — Aurora has built-in continuous backups
1010 NagSuppression(
1011 id="HIPAA.Security-RDSInBackupPlan",
1012 reason=(
1013 "Aurora Serverless v2 has built-in continuous backups with point-in-time "
1014 "recovery. AWS Backup integration is optional and can be enabled if required."
1015 ),
1016 ),
1017 NagSuppression(
1018 id="NIST.800.53.R5-RDSInBackupPlan",
1019 reason=(
1020 "Aurora Serverless v2 has built-in continuous backups with point-in-time "
1021 "recovery. AWS Backup integration is optional."
1022 ),
1023 ),
1024 # RDS logging enabled — covered by cloudwatch_logs_exports=["postgresql"]
1025 # but some frameworks check for additional log types
1026 NagSuppression(
1027 id="HIPAA.Security-RDSLoggingEnabled",
1028 reason=(
1029 "PostgreSQL logs are exported to CloudWatch via cloudwatch_logs_exports. "
1030 "Aurora Serverless v2 does not support all log types available on provisioned instances."
1031 ),
1032 ),
1033 NagSuppression(
1034 id="NIST.800.53.R5-RDSLoggingEnabled",
1035 reason=(
1036 "PostgreSQL logs are exported to CloudWatch via cloudwatch_logs_exports. "
1037 "Aurora Serverless v2 does not support all log types available on provisioned instances."
1038 ),
1039 ),
1040 NagSuppression(
1041 id="PCI.DSS.321-RDSLoggingEnabled",
1042 reason=(
1043 "PostgreSQL logs are exported to CloudWatch via cloudwatch_logs_exports. "
1044 "Aurora Serverless v2 does not support all log types available on provisioned instances."
1045 ),
1046 ),
1047 # CloudWatch Log Group encryption for Aurora logs
1048 NagSuppression(
1049 id="HIPAA.Security-CloudWatchLogGroupEncrypted",
1050 reason=(
1051 "CloudWatch Logs for Aurora PostgreSQL are encrypted by default with "
1052 "AWS-managed keys. Customer-managed KMS can be enabled if required."
1053 ),
1054 ),
1055 NagSuppression(
1056 id="NIST.800.53.R5-CloudWatchLogGroupEncrypted",
1057 reason=(
1058 "CloudWatch Logs for Aurora PostgreSQL are encrypted by default with "
1059 "AWS-managed keys."
1060 ),
1061 ),
1062 NagSuppression(
1063 id="PCI.DSS.321-CloudWatchLogGroupEncrypted",
1064 reason=(
1065 "CloudWatch Logs for Aurora PostgreSQL are encrypted by default with "
1066 "AWS-managed keys."
1067 ),
1068 ),
1069 # Enhanced monitoring IAM role uses AWS managed policy
1070 NagSuppression(
1071 id="AwsSolutions-IAM4",
1072 reason=(
1073 "Aurora enhanced monitoring requires the AWS managed policy "
1074 "AmazonRDSEnhancedMonitoringRole for publishing OS-level metrics to CloudWatch. "
1075 "This is the AWS-recommended policy for RDS enhanced monitoring. "
1076 "See: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_Monitoring.OS.Enabling.html"
1077 ),
1078 applies_to=[
1079 "Policy::arn:<AWS::Partition>:iam::aws:policy/service-role/AmazonRDSEnhancedMonitoringRole",
1080 ],
1081 ),
1082 ],
1083 )
1086def add_sagemaker_suppressions(
1087 stack: Stack,
1088 api_gateway_region: str | None = None,
1089 global_region: str | None = None,
1090 project_name: str = "gco",
1091) -> None:
1092 """Add suppressions for SageMaker Studio Domain + execution role findings.
1094 The analytics stack uses a private-VPC SageMaker Studio domain whose
1095 execution role needs wildcard access to a known set of ARN patterns:
1096 regional SQS job queues (one per regional stack, name pattern
1097 ``<project>-jobs-<region>``), GCO API Gateway GET routes (any REST API
1098 id under ``/prod/GET/api/v1/*``), and ``Cluster_Shared_Bucket`` objects
1099 resolved from cross-region SSM. Each wildcard is scoped on the literal
1100 patterns below so cdk-nag's ``AwsSolutions-IAM5`` check surfaces only
1101 the documented escape hatches.
1103 Args:
1104 stack: The analytics stack to apply suppressions to.
1105 api_gateway_region: Concrete region where the API Gateway stack
1106 lives (used to resolve the execute-api ARN pattern).
1107 global_region: Concrete global region (used to resolve the
1108 KMS ``ViaService`` condition's service endpoint — the KMS
1109 decrypt ARN itself is ``*`` because the cluster-shared KMS
1110 key lives in a different stack).
1111 """
1112 api_region = api_gateway_region or "*"
1113 gbl_region = global_region or "*"
1115 applies_to: list[str] = [
1116 # SageMaker execution role — SQS submit to any regional queue under
1117 # the project's ``<project>-jobs-*`` pattern. The SQS queue ARNs
1118 # are owned by the regional stacks and not directly importable.
1119 f"Resource::arn:<AWS::Partition>:sqs:*:<AWS::AccountId>:{project_name}-jobs-*",
1120 # SageMaker execution role — ``ssm:GetParameter`` on the
1121 # Cluster_Shared_Bucket metadata parameters under
1122 # ``/gco/cluster-shared-bucket/*`` in the global region. The path
1123 # wildcard covers exactly three literal parameter names
1124 # (name / arn / region) defined by ``GCOGlobalStack``; the rest of
1125 # the ARN is fully scoped (global region + account).
1126 f"Resource::arn:<AWS::Partition>:ssm:{gbl_region}:<AWS::AccountId>:parameter/{project_name}/cluster-shared-bucket/*",
1127 # SageMaker execution role — execute-api on any REST API id
1128 # under ``/prod/*/api/v1/*`` and ``/prod/*/inference/*`` in the
1129 # api-gateway region. The concrete region value is templated in
1130 # so the nag match works regardless of which region the user
1131 # deploys to. The HTTP-method segment is ``*`` (instead of
1132 # pinning ``GET``) so notebooks can submit jobs, update
1133 # templates, and manage inference endpoints in addition to
1134 # read-only GETs.
1135 f"Resource::arn:<AWS::Partition>:execute-api:{api_region}:<AWS::AccountId>:*/prod/*/api/v1/*",
1136 f"Resource::arn:<AWS::Partition>:execute-api:{api_region}:<AWS::AccountId>:*/prod/*/inference/*",
1137 # KMS decrypt scoped by
1138 # ``kms:ViaService=s3.<global-region>.<AWS::URLSuffix>``
1139 # condition — the resource ARN is unknown to this stack (cluster-
1140 # shared KMS key lives in the global region) so Resource::* is the
1141 # documented pattern, narrowed by the ViaService condition.
1142 "Resource::*",
1143 # S3 grant_read_write on Studio_Only_Bucket produces the AWS-
1144 # recommended set of S3 action wildcards. Each one covers a
1145 # closed, read-or-write intent on a single literal bucket ARN.
1146 "Action::s3:Abort*",
1147 "Action::s3:DeleteObject*",
1148 "Action::s3:GetBucket*",
1149 "Action::s3:GetObject*",
1150 "Action::s3:List*",
1151 # KMS grant_encrypt_decrypt on Analytics_KMS_Key produces the
1152 # AWS-recommended set of KMS action wildcards. Each covers a
1153 # single key ARN.
1154 "Action::kms:GenerateDataKey*",
1155 "Action::kms:ReEncrypt*",
1156 # Object-key wildcard on the literal Studio_Only_Bucket ARN — the
1157 # RW grant must cover every object key under the bucket.
1158 "Resource::<StudioOnlyBucket80FF5E65.Arn>/*",
1159 # ``kms:ViaService`` condition-scoped wildcard on the cluster-
1160 # shared bucket's KMS key — only matched when s3 is the invoking
1161 # service in the global region.
1162 f"Condition::kms:ViaService:s3.{gbl_region}.<AWS::URLSuffix>",
1163 # Studio UI actions — the execution role is assumed by the Studio
1164 # runtime and needs domain/space/app/user-profile wildcards to
1165 # render the IDE and manage notebook apps.
1166 f"Resource::arn:<AWS::Partition>:sagemaker:{api_region}:<AWS::AccountId>:domain/*",
1167 f"Resource::arn:<AWS::Partition>:sagemaker:{api_region}:<AWS::AccountId>:user-profile/*/*",
1168 f"Resource::arn:<AWS::Partition>:sagemaker:{api_region}:<AWS::AccountId>:space/*/*",
1169 f"Resource::arn:<AWS::Partition>:sagemaker:{api_region}:<AWS::AccountId>:app/*/*/*/*",
1170 # EMR Serverless — Studio discovers and manages EMR apps via these
1171 # actions. Resource::* is required because EMR Serverless does not
1172 # support resource-level scoping on most actions.
1173 "Action::emr-serverless:*",
1174 # SageMaker MLflow tracking servers + MLflow Apps. The
1175 # ``sagemaker-mlflow:*`` data-plane namespace is the one the
1176 # MLflow SDK talks to via SigV4 (``log_metric``,
1177 # ``log_artifact``, ``register_model``, etc.); the managed
1178 # ``AmazonSageMakerFullAccess`` policy covers the
1179 # ``sagemaker:*`` control-plane namespace (MLflow Apps,
1180 # Tracking Servers, Model Registry) but NOT the
1181 # ``sagemaker-mlflow`` service prefix, so this statement stays
1182 # inline and scoped to the api-gateway region + account.
1183 "Action::sagemaker-mlflow:*",
1184 f"Resource::arn:<AWS::Partition>:sagemaker:{api_region}:<AWS::AccountId>:mlflow-tracking-server/*",
1185 f"Resource::arn:<AWS::Partition>:sagemaker:{api_region}:<AWS::AccountId>:mlflow-app/*",
1186 # ``sts:GetCallerIdentity`` does not support resource-level
1187 # scoping; the MLflow SigV4 plug-in calls it on every request.
1188 "Action::sts:GetCallerIdentity",
1189 ]
1191 acknowledge_nag_findings(
1192 stack,
1193 [
1194 NagSuppression(
1195 id="AwsSolutions-IAM5",
1196 reason=(
1197 "SageMaker_Execution_Role uses wildcard ARNs and actions for: "
1198 "(1) SQS SendMessage on any regional job queue matching "
1199 "``<project>-jobs-<region>``, (2) execute-api:Invoke on "
1200 "any REST API id under /prod/*/api/v1/* and "
1201 "/prod/*/inference/* in the api-gateway region (all "
1202 "HTTP methods, so notebooks can submit jobs and "
1203 "manage inference endpoints in addition to read-only "
1204 "GETs), (3) KMS Decrypt/GenerateDataKey "
1205 "scoped by kms:ViaService=s3.<global-region>.<AWS::URLSuffix> "
1206 "condition (the cluster-shared KMS key ARN is not known "
1207 "to the analytics stack — it lives in the global region), "
1208 "(4) S3 action wildcards (``s3:Abort*``, ``s3:DeleteObject*``, "
1209 "``s3:GetBucket*``, ``s3:GetObject*``, ``s3:List*``) produced "
1210 "by ``bucket.grant_read_write(role)`` on the literal "
1211 "Studio_Only_Bucket ARN, (5) KMS action wildcards "
1212 "(``kms:GenerateDataKey*``, ``kms:ReEncrypt*``) produced by "
1213 "``kms_key.grant_encrypt_decrypt(role)`` on the literal "
1214 "Analytics_KMS_Key ARN, (6) ``<StudioOnlyBucket.Arn>/*`` "
1215 "object-key wildcard on the single literal bucket, and "
1216 "(7) ``ssm:GetParameter`` on "
1217 "``/gco/cluster-shared-bucket/*`` in the global region "
1218 "(covers exactly three literal parameter names — "
1219 "name/arn/region — defined by ``GCOGlobalStack``; lets "
1220 "Studio notebooks resolve the shared-bucket metadata at "
1221 "runtime without a per-user export step), (8) "
1222 "``sagemaker-mlflow:*`` on MLflow tracking server and "
1223 "MLflow App ARN wildcards in the api-gateway region + "
1224 "account so notebooks can log experiments, runs, "
1225 "metrics, artifacts, and registered-model versions "
1226 "via the MLflow SDK's SigV4 plug-in (the companion "
1227 "``sagemaker:*Mlflow*`` / ``sagemaker:*ModelPackage*`` "
1228 "control-plane actions are now attached via the "
1229 "``AmazonSageMakerFullAccess`` managed policy instead "
1230 "of enumerated inline), and (9) "
1231 "``sts:GetCallerIdentity`` on ``*`` which is required "
1232 "by MLflow's SigV4 plug-in and does not support "
1233 "resource-level scoping. Each wildcard is scoped on a "
1234 "narrow literal pattern."
1235 ),
1236 applies_to=applies_to,
1237 ),
1238 # SageMaker execution role does not require MFA — callers reach
1239 # the role through Cognito-gated presigned URLs rather
1240 # than direct AssumeRole calls from operator terminals.
1241 NagSuppression(
1242 id="AwsSolutions-IAM4",
1243 reason=(
1244 "SageMaker_Execution_Role does not attach AWS managed "
1245 "policies. The role is assumed only by sagemaker.amazonaws.com "
1246 "and used exclusively by notebooks running inside the "
1247 "Studio domain."
1248 ),
1249 ),
1250 # The Studio domain itself — VpcOnly network mode is the
1251 # primary security control; additional HIPAA/NIST checks that
1252 # assume a customer-managed image (``AwsSolutions-SM2`` etc.)
1253 # are suppressed because this deployment intentionally uses
1254 # the stock AWS-published SageMaker Distribution images.
1255 NagSuppression(
1256 id="AwsSolutions-SM2",
1257 reason=(
1258 "The Studio domain uses AWS-published stock SageMaker "
1259 "Distribution images and does not define custom "
1260 "images or app image configs. Per-user EFS access points "
1261 "give POSIX isolation without a custom image."
1262 ),
1263 ),
1264 NagSuppression(
1265 id="AwsSolutions-SM3",
1266 reason=(
1267 "SageMaker Studio domain is provisioned with "
1268 "``app_network_access_type=VpcOnly`` — all Studio traffic "
1269 "stays on the analytics stack's private-isolated VPC. "
1270 "Direct internet access is structurally unavailable."
1271 ),
1272 ),
1273 ],
1274 )
1276 # The separate SagemakerClusterSharedBucketGrant inline Policy (a
1277 # sibling construct to the role, created by
1278 # ``_grant_sagemaker_role_on_cluster_shared_bucket``) has its own
1279 # ``<ReadClusterSharedBucketArn*.Parameter.Value>/*`` object-key
1280 # wildcard on the literal cluster-shared bucket ARN resolved from
1281 # cross-region SSM. Resource-level scoping isn't possible here —
1282 # the parent role's resource suppression has ``apply_to_children``
1283 # semantics that only traverse CDK children, not siblings.
1284 acknowledge_nag_findings(
1285 stack,
1286 [
1287 NagSuppression(
1288 id="AwsSolutions-IAM5",
1289 reason=(
1290 "SagemakerClusterSharedBucketGrant attaches the RW "
1291 "policy on the single literal Cluster_Shared_Bucket "
1292 "ARN resolved from /gco/cluster-shared-bucket/arn. "
1293 "The ``<arn>/*`` object-key wildcard covers every "
1294 "object key inside the single always-on "
1295 "gco-cluster-shared-<account>-<region> bucket, "
1296 "identical in shape and intent to the regional stack's "
1297 "analogous job-pod grant."
1298 ),
1299 applies_to=[
1300 "Resource::<ReadClusterSharedBucketArn4B0BD291.Parameter.Value>/*",
1301 ],
1302 ),
1303 ],
1304 )
1307def add_cognito_suppressions(stack: Stack) -> None:
1308 """Add suppressions for Cognito user pool findings.
1310 Most Cognito-related checks are handled by
1311 ``advanced_security_mode=ENFORCED`` and the password-policy
1312 configuration set on the pool itself. Only a small number of
1313 structural findings need an explicit suppression — these are the ones
1314 that don't apply to a machine-to-machine + presigned-URL model where
1315 there is no hosted UI callback to harden.
1316 """
1317 acknowledge_nag_findings(
1318 stack,
1319 [
1320 NagSuppression(
1321 id="AwsSolutions-COG3",
1322 reason=(
1323 "The Cognito user pool has ``advanced_security_mode=ENFORCED`` "
1324 "which provides adaptive risk-based authentication, replacing "
1325 "the need for an additional MFA enforcement step at this level. "
1326 "Admins add MFA via ``gco analytics users add --require-mfa`` "
1327 "when required."
1328 ),
1329 ),
1330 # COG2 is WARN-level: "The Cognito user pool does not require
1331 # MFA." MFA is configured at the per-user level through the
1332 # ``gco analytics users add --require-mfa`` CLI path rather
1333 # than being enforced pool-wide; enforcing it at the pool
1334 # level would lock out admins bootstrapping the first user
1335 # during initial deploy.
1336 NagSuppression(
1337 id="AwsSolutions-COG2",
1338 reason=(
1339 "MFA is managed per-user through the ``gco analytics "
1340 "users add --require-mfa`` CLI command rather than "
1341 "enforced pool-wide. ``advanced_security_mode=ENFORCED`` "
1342 "provides adaptive risk-based authentication that "
1343 "triggers MFA challenges on suspicious sign-in attempts. "
1344 "Pool-wide MFA enforcement would lock out the first "
1345 "admin bootstrapping user during initial deploy."
1346 ),
1347 ),
1348 ],
1349 )
1352def add_analytics_vpc_suppressions(stack: Stack) -> None:
1353 """Add suppressions for the analytics VPC and its endpoints.
1355 The analytics VPC uses private subnets with NAT egress for notebook
1356 internet access (pip install, git clone) plus a small public subnet
1357 that hosts only the NAT gateway ENI. Findings on the public subnet
1358 and IGW route are expected — no compute runs there.
1359 """
1360 acknowledge_nag_findings(
1361 stack,
1362 [
1363 # Flow-logs suppressions — analytics VPC is private-isolated,
1364 # has no IGW/NAT, and every egress path is a VPC endpoint. The
1365 # service endpoints already emit CloudTrail data events that
1366 # cover every packet-producing API call on the VPC.
1367 NagSuppression(
1368 id="AwsSolutions-VPC7",
1369 reason=(
1370 "The analytics VPC is private-isolated (no IGW, no "
1371 "NAT Gateway). All egress flows through VPC interface/"
1372 "gateway endpoints for SageMaker, S3, STS, Logs, ECR, "
1373 "and EFS, each of which emits CloudTrail data events. "
1374 "Flow logs would duplicate that telemetry at "
1375 "significant storage cost without adding visibility."
1376 ),
1377 ),
1378 NagSuppression(
1379 id="HIPAA.Security-VPCFlowLogsEnabled",
1380 reason=(
1381 "The analytics VPC is private-isolated (no IGW, no "
1382 "NAT Gateway). All egress flows through VPC interface/"
1383 "gateway endpoints for SageMaker, S3, STS, Logs, ECR, "
1384 "and EFS, each of which emits CloudTrail data events. "
1385 "Flow logs would duplicate that telemetry at "
1386 "significant storage cost without adding visibility."
1387 ),
1388 ),
1389 NagSuppression(
1390 id="NIST.800.53.R5-VPCFlowLogsEnabled",
1391 reason=(
1392 "The analytics VPC is private-isolated (no IGW, no "
1393 "NAT Gateway). All egress flows through VPC interface/"
1394 "gateway endpoints for SageMaker, S3, STS, Logs, ECR, "
1395 "and EFS, each of which emits CloudTrail data events. "
1396 "Flow logs would duplicate that telemetry at "
1397 "significant storage cost without adding visibility."
1398 ),
1399 ),
1400 NagSuppression(
1401 id="PCI.DSS.321-VPCFlowLogsEnabled",
1402 reason=(
1403 "The analytics VPC is private-isolated (no IGW, no "
1404 "NAT Gateway). All egress flows through VPC interface/"
1405 "gateway endpoints for SageMaker, S3, STS, Logs, ECR, "
1406 "and EFS, each of which emits CloudTrail data events. "
1407 "Flow logs would duplicate that telemetry at "
1408 "significant storage cost without adding visibility."
1409 ),
1410 ),
1411 # Public subnet findings — the NAT gateway requires a public
1412 # subnet with an IGW route. No compute runs in the public
1413 # subnet; it only hosts the NAT gateway's ENI.
1414 NagSuppression(
1415 id="HIPAA.Security-VPCSubnetAutoAssignPublicIpDisabled",
1416 reason=(
1417 "The public subnet exists solely to host the NAT "
1418 "gateway ENI for notebook internet egress (pip install, "
1419 "git clone). No EC2 instances or Studio compute runs "
1420 "in this subnet."
1421 ),
1422 ),
1423 NagSuppression(
1424 id="NIST.800.53.R5-VPCSubnetAutoAssignPublicIpDisabled",
1425 reason=(
1426 "The public subnet exists solely to host the NAT "
1427 "gateway ENI. No compute workloads run here."
1428 ),
1429 ),
1430 NagSuppression(
1431 id="PCI.DSS.321-VPCSubnetAutoAssignPublicIpDisabled",
1432 reason=(
1433 "The public subnet exists solely to host the NAT "
1434 "gateway ENI. No compute workloads run here."
1435 ),
1436 ),
1437 NagSuppression(
1438 id="HIPAA.Security-VPCNoUnrestrictedRouteToIGW",
1439 reason=(
1440 "The 0.0.0.0/0 route to the IGW is in the public "
1441 "subnet's route table, which only hosts the NAT "
1442 "gateway. Private subnets route through NAT, not IGW."
1443 ),
1444 ),
1445 NagSuppression(
1446 id="NIST.800.53.R5-VPCNoUnrestrictedRouteToIGW",
1447 reason=(
1448 "The 0.0.0.0/0 route to the IGW is in the public "
1449 "subnet's route table for NAT gateway egress only."
1450 ),
1451 ),
1452 NagSuppression(
1453 id="PCI.DSS.321-VPCNoUnrestrictedRouteToIGW",
1454 reason=(
1455 "The 0.0.0.0/0 route to the IGW is in the public "
1456 "subnet's route table for NAT gateway egress only."
1457 ),
1458 ),
1459 ],
1460 )
1463def add_analytics_s3_suppressions(stack: Stack) -> None:
1464 """Add suppressions for ``Studio_Only_Bucket`` + access-logs bucket findings.
1466 The analytics stack owns two buckets:
1468 1. ``Studio_Only_Bucket`` — KMS-encrypted with ``Analytics_KMS_Key``,
1469 block public access, enforce SSL, versioned. Replication is not
1470 enabled because this bucket is the endpoint of the SageMaker
1471 workload; cross-region replication would double storage cost and
1472 introduce eventual-consistency behavior that breaks notebook
1473 save/load semantics.
1474 2. ``AnalyticsAccessLogsBucket`` — SSE-S3 encrypted because S3
1475 server-access-log delivery to a KMS-encrypted bucket requires
1476 additional log-delivery role plumbing that the CDK ``s3.Bucket``
1477 construct does not wire automatically. Replication is not enabled
1478 because the bucket is the log sink, not a data store.
1479 """
1480 acknowledge_nag_findings(
1481 stack,
1482 [
1483 # S3 replication suppressions — both buckets are single-
1484 # region by design. The Studio bucket is scoped to a single
1485 # deploy region (api-gateway region) and the access-logs
1486 # bucket is its log sink; cross-region replication is not
1487 # applicable to either.
1488 NagSuppression(
1489 id="HIPAA.Security-S3BucketReplicationEnabled",
1490 reason=(
1491 "Studio_Only_Bucket and its access-logs bucket are "
1492 "single-region by design. The Studio bucket is the "
1493 "endpoint of the SageMaker workload in the api-gateway "
1494 "region; cross-region replication would double storage "
1495 "cost without a corresponding availability gain (the "
1496 "Studio domain itself is single-region). The access-"
1497 "logs bucket is the log sink and is co-located with "
1498 "the data bucket by construction."
1499 ),
1500 ),
1501 NagSuppression(
1502 id="NIST.800.53.R5-S3BucketReplicationEnabled",
1503 reason=(
1504 "Studio_Only_Bucket and its access-logs bucket are "
1505 "single-region by design. The Studio bucket is the "
1506 "endpoint of the SageMaker workload in the api-gateway "
1507 "region; cross-region replication would double storage "
1508 "cost without a corresponding availability gain (the "
1509 "Studio domain itself is single-region). The access-"
1510 "logs bucket is the log sink and is co-located with "
1511 "the data bucket by construction."
1512 ),
1513 ),
1514 NagSuppression(
1515 id="PCI.DSS.321-S3BucketReplicationEnabled",
1516 reason=(
1517 "Studio_Only_Bucket and its access-logs bucket are "
1518 "single-region by design. The Studio bucket is the "
1519 "endpoint of the SageMaker workload in the api-gateway "
1520 "region; cross-region replication would double storage "
1521 "cost without a corresponding availability gain (the "
1522 "Studio domain itself is single-region). The access-"
1523 "logs bucket is the log sink and is co-located with "
1524 "the data bucket by construction."
1525 ),
1526 ),
1527 # Access-logs bucket KMS encryption suppressions — SSE-S3 is
1528 # the AWS-documented pattern for server-access-log delivery
1529 # sinks. Switching to SSE-KMS would require an additional
1530 # log-delivery role that the CDK ``s3.Bucket`` construct does
1531 # not wire automatically.
1532 NagSuppression(
1533 id="HIPAA.Security-S3DefaultEncryptionKMS",
1534 reason=(
1535 "The analytics access-logs bucket uses SSE-S3 because "
1536 "S3 server-access-log delivery to a KMS-encrypted "
1537 "bucket requires an additional log-delivery role "
1538 "plumbing that CDK does not wire by default. Studio_"
1539 "Only_Bucket (the actual data bucket) IS KMS-encrypted "
1540 "with ``Analytics_KMS_Key``."
1541 ),
1542 ),
1543 NagSuppression(
1544 id="NIST.800.53.R5-S3DefaultEncryptionKMS",
1545 reason=(
1546 "The analytics access-logs bucket uses SSE-S3 because "
1547 "S3 server-access-log delivery to a KMS-encrypted "
1548 "bucket requires an additional log-delivery role "
1549 "plumbing that CDK does not wire by default. Studio_"
1550 "Only_Bucket (the actual data bucket) IS KMS-encrypted "
1551 "with ``Analytics_KMS_Key``."
1552 ),
1553 ),
1554 NagSuppression(
1555 id="PCI.DSS.321-S3DefaultEncryptionKMS",
1556 reason=(
1557 "The analytics access-logs bucket uses SSE-S3 because "
1558 "S3 server-access-log delivery to a KMS-encrypted "
1559 "bucket requires an additional log-delivery role "
1560 "plumbing that CDK does not wire by default. Studio_"
1561 "Only_Bucket (the actual data bucket) IS KMS-encrypted "
1562 "with ``Analytics_KMS_Key``."
1563 ),
1564 ),
1565 ],
1566 )
1569def add_presigned_url_lambda_suppressions(
1570 stack: Stack, api_gateway_region: str | None = None
1571) -> None:
1572 """Add suppressions for the analytics presigned-URL Lambda role.
1574 The Lambda needs wildcard access to SageMaker domain and user-profile
1575 ARNs because ``CreatePresignedDomainUrl``, ``DescribeUserProfile``,
1576 and ``CreateUserProfile`` all take ARN shapes that can only be
1577 resolved at invoke time from the incoming Cognito username. At synth
1578 time, ``domain/*`` and ``user-profile/*/*`` are the tightest literal
1579 ARN shapes we can bind in the IAM policy.
1580 """
1581 region = api_gateway_region or "*"
1582 acknowledge_nag_findings(
1583 stack,
1584 [
1585 NagSuppression(
1586 id="AwsSolutions-IAM5",
1587 reason=(
1588 "The presigned-URL Lambda role uses SageMaker ARN "
1589 "wildcards on ``domain/*`` and ``user-profile/*/*`` "
1590 "because DomainId and UserProfileName are only "
1591 "resolvable at invoke time from the incoming Cognito "
1592 "username. ``ListDomains`` does not support resource-"
1593 "level scoping — the AWS API only accepts Resource::* "
1594 "— so a ``Resource::*`` suppression is required for "
1595 "that specific action. The effective blast radius is "
1596 "a single paginated list call per Lambda invocation "
1597 "against this account's SageMaker control plane in "
1598 "the api-gateway region."
1599 ),
1600 applies_to=[
1601 "Resource::*",
1602 (
1603 f"Resource::arn:<AWS::Partition>:sagemaker:{region}:<AWS::AccountId>:domain/*"
1604 ),
1605 (
1606 f"Resource::arn:<AWS::Partition>:sagemaker:{region}:<AWS::AccountId>:user-profile/*/*"
1607 ),
1608 # Generic shapes — catch tokenized-region variants
1609 # (``<AWS::Region>``) produced when CDK synthesizes
1610 # the policy without pinning the stack's env region.
1611 (
1612 "Resource::arn:<AWS::Partition>:sagemaker:<AWS::Region>:<AWS::AccountId>:domain/*"
1613 ),
1614 (
1615 "Resource::arn:<AWS::Partition>:sagemaker:<AWS::Region>:<AWS::AccountId>:user-profile/*/*"
1616 ),
1617 ],
1618 ),
1619 ],
1620 )
1623def add_emr_serverless_suppressions(stack: Stack) -> None:
1624 """Add suppressions for EMR Serverless Application findings.
1626 EMR Serverless doesn't have the same set of nag rules as EKS or Lambda;
1627 the main structural findings relate to the application's network
1628 configuration (which we pin to the private-isolated subnets + a
1629 dedicated SG) and the release-label pinning (covered by a constant in
1630 ``gco.stacks.constants``).
1631 """
1632 acknowledge_nag_findings(
1633 stack,
1634 [
1635 # Placeholder — EMR Serverless currently has no nag rules that
1636 # fire on a plain ``CfnApplication`` built against private
1637 # subnets. This helper exists so the analytics branch in
1638 # ``apply_all_suppressions`` has a single, predictable entry
1639 # point for EMR Serverless — future EMR-related rules land
1640 # here without touching the branch dispatch.
1641 NagSuppression(
1642 id="AwsSolutions-EMR1",
1643 reason=(
1644 "EMR Serverless application is created with explicit "
1645 "private-isolated subnet ids and a dedicated security "
1646 "group — the application never lands on public subnets."
1647 ),
1648 ),
1649 ],
1650 )
1653def apply_all_suppressions(
1654 stack: Stack,
1655 stack_type: str = "regional",
1656 regions: list[str] | None = None,
1657 global_region: str | None = None,
1658 api_gateway_region: str | None = None,
1659 project_name: str = "gco",
1660) -> None:
1661 """Apply all relevant suppressions to a stack.
1663 Args:
1664 stack: The CDK stack to apply suppressions to
1665 stack_type: Type of stack - 'regional', 'global', 'api_gateway',
1666 'regional_api_gateway', 'monitoring', or 'analytics'
1667 regions: List of regional deployment regions (for dynamic IAM suppression patterns)
1668 global_region: Global region for SSM parameters (for dynamic IAM suppression patterns)
1669 api_gateway_region: API Gateway region (for analytics stack — used to
1670 scope SageMaker execute-api and presigned-URL Lambda ARN patterns)
1671 project_name: Deployment prefix (#139), forwarded to
1672 ``add_iam_suppressions`` so the IAM allow-list ARN patterns match
1673 the deployment's project-scoped resource names. Defaults to
1674 ``"gco"``.
1675 """
1676 # Common suppressions for all stacks
1677 add_lambda_suppressions(stack)
1678 add_iam_suppressions(
1679 stack,
1680 regions=regions,
1681 global_region=global_region,
1682 api_gateway_region=api_gateway_region,
1683 project_name=project_name,
1684 )
1686 if stack_type == "regional":
1687 add_eks_suppressions(stack)
1688 add_eks_cluster_suppressions(stack)
1689 add_vpc_suppressions(stack)
1690 add_storage_suppressions(stack)
1691 add_sqs_suppressions(stack)
1692 add_aurora_pgvector_suppressions(stack)
1694 elif stack_type == "global":
1695 add_backup_suppressions(stack)
1697 elif stack_type == "api_gateway":
1698 add_api_gateway_suppressions(stack)
1699 add_secrets_suppressions(stack)
1701 elif stack_type == "regional_api_gateway":
1702 add_api_gateway_suppressions(stack)
1704 elif stack_type == "monitoring":
1705 add_monitoring_suppressions(stack)
1707 elif stack_type == "analytics": 1707 ↛ exitline 1707 didn't return from function 'apply_all_suppressions' because the condition on line 1707 was always true
1708 # Analytics stack has S3 buckets (Studio_Only + access-logs), KMS,
1709 # EFS, Cognito, SageMaker, EMR Serverless, and the presigned-URL
1710 # Lambda. Each helper scopes its own applies_to list.
1711 add_storage_suppressions(stack)
1712 add_sagemaker_suppressions(
1713 stack,
1714 api_gateway_region=api_gateway_region,
1715 global_region=global_region,
1716 project_name=project_name,
1717 )
1718 add_cognito_suppressions(stack)
1719 add_emr_serverless_suppressions(stack)
1720 add_analytics_vpc_suppressions(stack)
1721 add_analytics_s3_suppressions(stack)
1722 add_presigned_url_lambda_suppressions(stack, api_gateway_region=api_gateway_region)