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

140 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-10-28 16:19 +0000

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

2 

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

4that are intentionally not applicable or have documented justifications. 

5 

6Supported Compliance Frameworks: 

7- AWS Solutions: Best practices for AWS architectures 

8- HIPAA Security: Healthcare compliance requirements 

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

10- PCI DSS 3.2.1: Payment card industry standards 

11- Serverless: Best practices for serverless architectures 

12 

13Suppression Categories: 

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

152. Inline Policies - CDK-generated for custom resources 

163. Wildcard Permissions - Required for dynamic resource access 

174. Infrastructure Patterns - Intentional architectural decisions 

18""" 

19 

20from __future__ import annotations 

21 

22from collections.abc import Mapping, Sequence 

23from dataclasses import dataclass, field 

24from typing import Any 

25 

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 

40 

41# --------------------------------------------------------------------------- 

42# cdk-nag v3 acknowledgment mechanism 

43# --------------------------------------------------------------------------- 

44# cdk-nag v3 rewrote its engine from an ``IAspect`` to an 

45# ``IPolicyValidationPlugin`` (CDK's native policy-validation framework). The 

46# old ``NagSuppressions.add_(resource|stack)_suppressions`` / 

47# ``NagPackSuppression`` API is gone. Suppressions are now *acknowledgments* 

48# recorded as construct metadata under a well-known key; cdk-nag's 

49# ``isAcknowledged`` walks a construct's ancestor tree looking for that key, so 

50# an acknowledgment placed on a stack (or a role) covers every matching finding 

51# on that construct **and all of its descendants** — which is why there is no 

52# ``apply_to_children`` flag anymore (it is always effectively ``True``). 

53# 

54# Finding ids come in two shapes: 

55# * scalar rules -> the bare rule id, e.g. ``AwsSolutions-EC23`` 

56# * array rules -> ``<rule>[<detail>]``, e.g. 

57# ``AwsSolutions-IAM5[Resource::*]`` — matched EXACTLY 

58# (there is no bare-id fallback for these, and no regex 

59# matching either: every detail must be spelled out to the 

60# exact string cdk-nag emits, including any synthesis-time 

61# logical-id hash such as 

62# ``Resource::<RegionalSharedBucket3FF19783.Arn>/*``). 

63# 

64# We record acknowledgments by writing the metadata key ourselves via 

65# ``node.add_metadata`` rather than calling ``Validations.of(x).acknowledge``. 

66# Every detail we scope starts with ``Resource::`` / ``Policy::`` / 

67# ``Action::`` / ``Condition::`` — all contain ``::``, and ``acknowledge()`` 

68# rejects any id containing ``::`` with ``InvalidValidationId`` 

69# (https://github.com/cdklabs/cdk-nag/issues/2351). Writing the metadata key 

70# directly is the documented workaround and applies uniformly to every 

71# suppression, scalar or array. 

72 

73# The construct-metadata key cdk-nag v3 reads for acknowledged findings. 

74_ACK_METADATA_KEY: str = Validations.ACKNOWLEDGED_RULES_METADATA_KEY 

75 

76 

77@dataclass(frozen=True) 

78class NagSuppression: 

79 """A single cdk-nag v3 finding acknowledgment. 

80 

81 Args: 

82 id: The rule id to acknowledge (e.g. ``"AwsSolutions-IAM5"``). 

83 reason: Human-readable justification recorded alongside the 

84 acknowledgment. 

85 applies_to: Optional finding *details* to scope the acknowledgment. 

86 Each entry is matched verbatim against the ``<rule>[<detail>]`` 

87 finding id cdk-nag emits, so it must be the exact string — 

88 including any synthesis-time logical-id hash (e.g. 

89 ``Resource::<RegionalSharedBucket3FF19783.Arn>/*``). When empty, 

90 the bare rule id is acknowledged (correct for scalar rules that 

91 emit no ``[detail]`` suffix, such as ``AwsSolutions-EC23``). 

92 """ 

93 

94 id: str 

95 reason: str 

96 applies_to: Sequence[str] = field(default_factory=tuple) 

97 

98 

99def _normalize(supp: NagSuppression | Mapping[str, Any]) -> tuple[str, str, list[str]]: 

100 """Normalize a suppression to ``(rule_id, reason, details)``. 

101 

102 Accepts either a :class:`NagSuppression` or a plain mapping with ``id`` / 

103 ``reason`` / ``applies_to`` (or the jsii spelling ``appliesTo`` used by the 

104 resource-scoped call sites). Every ``applies_to`` entry must be an exact 

105 string — cdk-nag v3 matches finding details verbatim (there is no regex 

106 support). 

107 """ 

108 if isinstance(supp, NagSuppression): 

109 rule_id, reason, entries = supp.id, supp.reason, list(supp.applies_to) 

110 else: 

111 rule_id = supp["id"] 

112 reason = supp["reason"] 

113 entries = list(supp.get("applies_to") or supp.get("appliesTo") or []) 

114 

115 for entry in entries: 

116 if not isinstance(entry, str): 

117 raise ValueError( 

118 f"Unsupported applies_to entry for {rule_id!r}: {entry!r} " 

119 "(expected an exact detail string; cdk-nag v3 has no regex support)" 

120 ) 

121 return rule_id, reason, entries 

122 

123 

124def acknowledge_nag_findings( 

125 scope: IConstruct, 

126 suppressions: Sequence[NagSuppression | Mapping[str, Any]], 

127) -> None: 

128 """Record cdk-nag v3 acknowledgments on ``scope`` (covers its descendants). 

129 

130 This is GCO's v3-native replacement for the removed 

131 ``NagSuppressions.add_resource_suppressions`` / 

132 ``add_stack_suppressions``. Each finding id (``<rule>[<detail>]`` for 

133 scoped details, or the bare ``<rule>`` when ``applies_to`` is empty) is 

134 written to the cdk-nag acknowledgment metadata key, which every rule pack 

135 honors natively. Because cdk-nag walks the ancestor tree, an 

136 acknowledgment on a stack covers all resources in that stack, and one on a 

137 role covers the role's generated policies. 

138 """ 

139 # cdk-nag renders the CloudFormation partition/account/region 

140 # pseudo-parameters in a finding's detail two different ways, and which one 

141 # appears is decided per ARN, not per stack: 

142 # * an ARN built from ``Aws.PARTITION`` / ``Aws.ACCOUNT_ID`` / 

143 # ``Aws.REGION`` may retain the angle-bracket pseudo-param literal, 

144 # whereas 

145 # * an ARN hand-built from ``stack.partition`` / ``stack.account`` / 

146 # ``stack.region`` renders concrete values when CDK can resolve them. 

147 # So for a concrete env we can't know from here which form cdk-nag will emit 

148 # for any given finding. Register the acknowledgment under both the 

149 # placeholder detail and every resolvable literal rendering. Extra keys 

150 # that match no finding are harmless; whichever exact form cdk-nag emits 

151 # then has a matching acknowledgment. Any detail built from a raw token is 

152 # normalized first because unresolved tokens cannot be metadata-map keys. 

153 stack = Stack.of(scope) 

154 dimensions = ( 

155 (stack.partition, "<AWS::Partition>"), 

156 (stack.account, "<AWS::AccountId>"), 

157 (stack.region, "<AWS::Region>"), 

158 ) 

159 

160 def _keys_for(rule_id: str, detail: str) -> list[str]: 

161 for value, placeholder in dimensions: 

162 if Token.is_unresolved(value): 

163 detail = detail.replace(value, placeholder) 

164 if Token.is_unresolved(detail): 

165 raise ValueError( 

166 "cdk-nag acknowledgment detail contains an unresolved token and " 

167 f"cannot be used as a metadata key: {detail!r}. Use cdk-nag's " 

168 "literal rendering (e.g. '<AWS::Region>', '<LogicalId.Arn>') instead." 

169 ) 

170 # Expand each placeholder into every concrete environment dimension 

171 # CDK resolved, producing the exact partition/account/region combinations 

172 # cdk-nag can render for the same policy resource. 

173 variants = {detail} 

174 for value, placeholder in dimensions: 

175 if not Token.is_unresolved(value): 

176 variants.update(v.replace(placeholder, value) for v in list(variants)) 

177 return [f"{rule_id}[{v}]" for v in variants] 

178 

179 ack: dict[str, str] = {} 

180 for supp in suppressions: 

181 rule_id, reason, details = _normalize(supp) 

182 if not details: 

183 ack[rule_id] = reason 

184 for detail in details: 

185 for key in _keys_for(rule_id, detail): 

186 ack[key] = reason 

187 if ack: 

188 scope.node.add_metadata(_ACK_METADATA_KEY, ack) 

189 

190 

191# The cdk-nag rules that evaluate security-group *ingress CIDRs*. When an 

192# ingress rule's CIDR is a CloudFormation token (e.g. a VPC CIDR resolved via 

193# ``Fn::GetAtt``), these rules cannot resolve it to a primitive value and 

194# *throw*. cdk-nag v3 surfaces a thrown rule under its bare id (the v2 

195# ``CdkNagValidationFailure`` aggregate rule is gone), so a single unresolvable 

196# ingress rule produces one bare finding per rule below. Every GCO security 

197# group whose ingress is pinned to its own VPC CIDR trips this exact set. 

198_SECURITY_GROUP_CIDR_RULES: tuple[str, ...] = ( 

199 "AwsSolutions-EC23", 

200 "HIPAA.Security-EC2RestrictedCommonPorts", 

201 "HIPAA.Security-EC2RestrictedSSH", 

202 "NIST.800.53.R5-EC2RestrictedCommonPorts", 

203 "NIST.800.53.R5-EC2RestrictedSSH", 

204 "PCI.DSS.321-EC2RestrictedCommonPorts", 

205 "PCI.DSS.321-EC2RestrictedSSH", 

206) 

207 

208 

209def acknowledge_security_group_cidr_findings(scope: IConstruct, *, reason: str) -> None: 

210 """Acknowledge the SG-ingress rules that throw on a token (VPC CIDR) source. 

211 

212 Scope this to the specific security-group-bearing construct — an EKS 

213 cluster, a VPC whose interface endpoints carry security groups, or a 

214 standalone ``SecurityGroup`` — rather than the whole stack, so the bare-id 

215 acknowledgment cannot mask a genuine open-ingress finding elsewhere. 

216 cdk-nag walks the ancestor tree, so the acknowledgment covers every ingress 

217 rule under ``scope``. 

218 """ 

219 acknowledge_nag_findings( 

220 scope, 

221 [NagSuppression(id=rule, reason=reason) for rule in _SECURITY_GROUP_CIDR_RULES], 

222 ) 

223 

224 

225def nag_validation_plugins( 

226 scope: IConstruct, *, verbose: bool = True 

227) -> list[IPolicyValidationPlugin]: 

228 """Return the five GCO cdk-nag rule packs as v3 policy-validation plugins. 

229 

230 Register with ``Validations.of(app).add_plugins(*nag_validation_plugins(app))``. 

231 Each pack reads the acknowledgment metadata written by 

232 :func:`acknowledge_nag_findings` natively, so the packs run directly with 

233 no wrapping. 

234 """ 

235 return [ 

236 AwsSolutionsChecks(scope, verbose=verbose), 

237 HIPAASecurityChecks(scope, verbose=verbose), 

238 NIST80053R5Checks(scope, verbose=verbose), 

239 PCIDSS321Checks(scope, verbose=verbose), 

240 ServerlessChecks(scope, verbose=verbose), 

241 ] 

242 

243 

244def suppress_managed_policy_opt_in( 

245 resource: IConstruct, 

246 *, 

247 managed_policy_name: str, 

248 reason: str, 

249) -> None: 

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

251 

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

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

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

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

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

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

258 scoped suppression rather than a broad one. 

259 

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

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

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

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

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

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

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

267 

268 Args: 

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

270 managed-policy attachment. 

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

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

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

274 helper adds that. 

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

276 the managed policy is preferred over an enumerated 

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

278 conditional is that gates the attachment (so reviewers 

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

280 """ 

281 acknowledge_nag_findings( 

282 resource, 

283 [ 

284 NagSuppression( 

285 id="AwsSolutions-IAM4", 

286 reason=reason, 

287 applies_to=[ 

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

289 ], 

290 ), 

291 ], 

292 ) 

293 

294 

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

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

297 

298 EKS requires specific AWS managed policies that cannot be replaced 

299 with customer-managed policies without breaking functionality. 

300 """ 

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

302 eks_managed_policies = [ 

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

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

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

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

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

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

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

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

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

312 # CloudWatch Observability addon policies for Container Insights 

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

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

315 ] 

316 

317 acknowledge_nag_findings( 

318 stack, 

319 [ 

320 NagSuppression( 

321 id="AwsSolutions-IAM4", 

322 reason=( 

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

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

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

326 ), 

327 applies_to=eks_managed_policies, 

328 ), 

329 ], 

330 ) 

331 

332 

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

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

335 

336 Lambda functions used for CDK custom resources and infrastructure 

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

338 """ 

339 lambda_managed_policies = [ 

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

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

342 ] 

343 

344 acknowledge_nag_findings( 

345 stack, 

346 [ 

347 NagSuppression( 

348 id="AwsSolutions-IAM4", 

349 reason=( 

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

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

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

353 ), 

354 applies_to=lambda_managed_policies, 

355 ), 

356 NagSuppression( 

357 id="AwsSolutions-L1", 

358 reason=( 

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

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

361 ), 

362 ), 

363 # HIPAA Lambda suppressions 

364 NagSuppression( 

365 id="HIPAA.Security-LambdaConcurrency", 

366 reason=( 

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

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

369 ), 

370 ), 

371 NagSuppression( 

372 id="HIPAA.Security-LambdaDLQ", 

373 reason=( 

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

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

376 ), 

377 ), 

378 NagSuppression( 

379 id="HIPAA.Security-LambdaInsideVPC", 

380 reason=( 

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

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

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

384 ), 

385 ), 

386 # NIST 800-53 Lambda suppressions 

387 NagSuppression( 

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

389 reason=( 

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

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

392 ), 

393 ), 

394 NagSuppression( 

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

396 reason=( 

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

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

399 ), 

400 ), 

401 NagSuppression( 

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

403 reason=( 

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

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

406 ), 

407 ), 

408 # PCI DSS Lambda suppressions 

409 NagSuppression( 

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

411 reason=( 

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

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

414 ), 

415 ), 

416 # Serverless Lambda suppressions 

417 NagSuppression( 

418 id="Serverless-LambdaLatestVersion", 

419 reason=( 

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

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

422 ), 

423 ), 

424 NagSuppression( 

425 id="Serverless-LambdaDefaultMemorySize", 

426 reason=( 

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

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

429 ), 

430 ), 

431 NagSuppression( 

432 id="Serverless-LambdaDLQ", 

433 reason=( 

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

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

436 ), 

437 ), 

438 ], 

439 ) 

440 

441 

442def add_iam_suppressions( 

443 stack: Stack, 

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

445 global_region: str | None = None, 

446 project_name: str = "gco", 

447) -> None: 

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

449 

450 CDK generates inline policies for custom resources and some patterns 

451 require wildcard permissions for dynamic resource access. 

452 

453 Args: 

454 stack: The CDK stack to apply suppressions to 

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

456 global_region: Global region for SSM parameters and DynamoDB tables 

457 project_name: Deployment prefix (#139). Every allow-list ARN pattern 

458 below (``/<project>/`` SSM tree, ``<project>-*`` DynamoDB tables 

459 and S3 buckets) is scoped to this so the suppression matches the 

460 deployment's actual, project-scoped resource names. Defaults to 

461 ``"gco"`` so the rendered patterns are byte-for-byte identical to 

462 the pre-#139 literals for the stock deployment. 

463 """ 

464 # The API Gateway auth secret needs no entry here: its read grants end in 

465 # ``-??????`` rather than ``*`` (see ``api_gateway_auth_secret_name``), and 

466 # AwsSolutions-IAM5 only flags ``*``. 

467 # 

468 # Build dynamic applies_to list based on configured regions 

469 applies_to = [ 

470 # The convergence Step Functions state machine's role invokes each 

471 # Lambda task's versions — CDK's LambdaInvoke grants `<fn>.Arn:*` for 

472 # the version/alias qualifier, which is what these `:*` findings flag. 

473 # kubectl-applier: the base and post-Helm manifest passes. 

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

475 # GA-registration: the final Global Accelerator registration task. 

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

477 # Delete-time GA deregistration guard (issue #130): its cr.Provider 

478 # framework-onEvent role invokes the deregistration Lambda's versions. 

479 "Resource::<GaDeregistrationFunction5CFAADA4.Arn>:*", 

480 # helm-installer: one task per Helm chart. 

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

482 # VPC Flow Logs delivery role writes log events to every stream in the 

483 # flow-log group (logs:CreateLogStream/PutLogEvents on `<group>.Arn:*`). 

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

485 ] 

486 

487 # Add EKS addon patterns for each configured region 

488 if regions: 

489 for region in regions: 

490 applies_to.append( 

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

492 ) 

493 

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

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

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

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

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

499 ssm_regions_set: set[str] = set() 

500 if global_region: 

501 ssm_regions_set.add(global_region) 

502 if regions: 

503 ssm_regions_set.update(regions) 

504 

505 for region in sorted(ssm_regions_set): 

506 applies_to.append( 

507 f"Resource::arn:<AWS::Partition>:ssm:{region}:<AWS::AccountId>:parameter/{project_name}/*" 

508 ) 

509 # Per-chart add-on status + replay input written by the helm installer 

510 # and orchestrator (gco stacks addons status/install). Scoped to the 

511 # project's addons subtree in each region. 

512 applies_to.append( 

513 f"Resource::arn:<AWS::Partition>:ssm:{region}:<AWS::AccountId>:parameter/{project_name}/addons/*" 

514 ) 

515 

516 # Add DynamoDB index wildcard patterns for global region 

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

518 if global_region: 

519 applies_to.extend( 

520 [ 

521 f"Resource::arn:<AWS::Partition>:dynamodb:{global_region}:<AWS::AccountId>:table/{project_name}-job-templates/index/*", 

522 f"Resource::arn:<AWS::Partition>:dynamodb:{global_region}:<AWS::AccountId>:table/{project_name}-webhooks/index/*", 

523 f"Resource::arn:<AWS::Partition>:dynamodb:{global_region}:<AWS::AccountId>:table/{project_name}-jobs/index/*", 

524 f"Resource::arn:<AWS::Partition>:dynamodb:{global_region}:<AWS::AccountId>:table/{project_name}-inference-endpoints/index/*", 

525 ] 

526 ) 

527 

528 # Add S3 wildcard patterns for the project's buckets. CDK auto-generates 

529 # some bucket names from the stack name (``<project_name>-<region>``), and 

530 # the always-on buckets use explicit ``<project_name>-...`` names, so a 

531 # single ``<project_name>-*`` prefix covers both. 

532 applies_to.extend( 

533 [ 

534 f"Resource::arn:<AWS::Partition>:s3:::{project_name}-*", 

535 f"Resource::arn:<AWS::Partition>:s3:::{project_name}-*/*", 

536 ] 

537 ) 

538 

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

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

541 

542 acknowledge_nag_findings( 

543 stack, 

544 [ 

545 # Inline policy suppressions for all frameworks 

546 NagSuppression( 

547 id="HIPAA.Security-IAMNoInlinePolicy", 

548 reason=( 

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

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

551 ), 

552 ), 

553 NagSuppression( 

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

555 reason=( 

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

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

558 ), 

559 ), 

560 NagSuppression( 

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

562 reason=( 

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

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

565 ), 

566 ), 

567 # Wildcard permission suppressions 

568 NagSuppression( 

569 id="AwsSolutions-IAM5", 

570 reason=( 

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

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

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

574 "(5) VPC Flow Logs to write to CloudWatch, (6) DynamoDB GSI access for job queue, " 

575 "templates, webhooks, and inference endpoints tables, (7) S3 access for model " 

576 "weights bucket (auto-generated name). All wildcards are scoped to specific " 

577 "patterns. (8) KMS decrypt scoped to S3 via condition for model weights bucket." 

578 ), 

579 applies_to=applies_to, 

580 ), 

581 ], 

582 ) 

583 

584 

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

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

587 

588 Public subnets and IGW routes host the NAT gateways that provide bounded 

589 egress for private EKS nodes and VPC Lambdas. The platform ALB remains internal. 

590 """ 

591 acknowledge_nag_findings( 

592 stack, 

593 [ 

594 # HIPAA VPC suppressions 

595 NagSuppression( 

596 id="HIPAA.Security-VPCSubnetAutoAssignPublicIpDisabled", 

597 reason=( 

598 "Public subnets host NAT gateways that provide controlled egress " 

599 "for private EKS nodes and VPC Lambdas; workloads and the platform " 

600 "ALB remain in private subnets." 

601 ), 

602 ), 

603 NagSuppression( 

604 id="HIPAA.Security-VPCNoUnrestrictedRouteToIGW", 

605 reason=( 

606 "Public-subnet NAT gateways require an Internet Gateway route to " 

607 "provide outbound dependency access for private subnets. EKS nodes, " 

608 "VPC Lambdas, and the platform ALB remain private." 

609 ), 

610 ), 

611 # NIST 800-53 VPC suppressions 

612 NagSuppression( 

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

614 reason=( 

615 "Public subnets host NAT gateways that provide controlled egress " 

616 "for private EKS nodes and VPC Lambdas; workloads and the platform " 

617 "ALB remain in private subnets." 

618 ), 

619 ), 

620 NagSuppression( 

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

622 reason=( 

623 "Public-subnet NAT gateways require an Internet Gateway route to " 

624 "provide outbound dependency access for private subnets. EKS nodes, " 

625 "VPC Lambdas, and the platform ALB remain private." 

626 ), 

627 ), 

628 # PCI DSS VPC suppressions 

629 NagSuppression( 

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

631 reason=( 

632 "Public subnets host NAT gateways that provide controlled egress " 

633 "for private EKS nodes and VPC Lambdas; workloads and the platform " 

634 "ALB remain in private subnets." 

635 ), 

636 ), 

637 NagSuppression( 

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

639 reason=( 

640 "Public-subnet NAT gateways require an Internet Gateway route to " 

641 "provide outbound dependency access for private subnets. EKS nodes, " 

642 "VPC Lambdas, and the platform ALB remain private." 

643 ), 

644 ), 

645 ], 

646 ) 

647 

648 

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

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

651 acknowledge_nag_findings( 

652 stack, 

653 [ 

654 NagSuppression( 

655 id="AwsSolutions-COG4", 

656 reason=( 

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

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

659 ), 

660 ), 

661 NagSuppression( 

662 id="AwsSolutions-APIG2", 

663 reason=( 

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

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

666 ), 

667 ), 

668 # Cache suppressions - caching is intentionally disabled 

669 NagSuppression( 

670 id="HIPAA.Security-APIGWCacheEnabledAndEncrypted", 

671 reason=( 

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

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

674 ), 

675 ), 

676 NagSuppression( 

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

678 reason=( 

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

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

681 ), 

682 ), 

683 NagSuppression( 

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

685 reason=( 

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

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

688 ), 

689 ), 

690 # SSL certificate suppressions 

691 NagSuppression( 

692 id="HIPAA.Security-APIGWSSLEnabled", 

693 reason=( 

694 "API Gateway integrates with Lambda rather than an HTTP backend that " 

695 "requires an API Gateway client certificate. The public API endpoint " 

696 "enforces HTTPS; Lambda signs the exact private-backend request with " 

697 "a short-lived HMAC envelope." 

698 ), 

699 ), 

700 NagSuppression( 

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

702 reason=( 

703 "API Gateway integrates with Lambda rather than an HTTP backend that " 

704 "requires an API Gateway client certificate. The public API endpoint " 

705 "enforces HTTPS; Lambda signs the exact private-backend request with " 

706 "a short-lived HMAC envelope." 

707 ), 

708 ), 

709 NagSuppression( 

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

711 reason=( 

712 "API Gateway integrates with Lambda rather than an HTTP backend that " 

713 "requires an API Gateway client certificate. The public API endpoint " 

714 "enforces HTTPS; Lambda signs the exact private-backend request with " 

715 "a short-lived HMAC envelope." 

716 ), 

717 ), 

718 # CloudWatch Log Group encryption suppressions 

719 NagSuppression( 

720 id="HIPAA.Security-CloudWatchLogGroupEncrypted", 

721 reason=( 

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

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

724 ), 

725 ), 

726 NagSuppression( 

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

728 reason=( 

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

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

731 ), 

732 ), 

733 NagSuppression( 

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

735 reason=( 

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

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

738 ), 

739 ), 

740 # API Gateway CloudWatch role 

741 NagSuppression( 

742 id="AwsSolutions-IAM4", 

743 reason=( 

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

745 "AmazonAPIGatewayPushToCloudWatchLogs for logging functionality." 

746 ), 

747 applies_to=[ 

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

749 ], 

750 ), 

751 ], 

752 ) 

753 

754 

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

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

757 acknowledge_nag_findings( 

758 stack, 

759 [ 

760 NagSuppression( 

761 id="AwsSolutions-SNS3", 

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

763 ), 

764 NagSuppression( 

765 id="HIPAA.Security-SNSEncryptedKMS", 

766 reason=( 

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

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

769 ), 

770 ), 

771 NagSuppression( 

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

773 reason=( 

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

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

776 ), 

777 ), 

778 NagSuppression( 

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

780 reason=( 

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

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

783 ), 

784 ), 

785 # CloudWatch Log Group encryption 

786 NagSuppression( 

787 id="HIPAA.Security-CloudWatchLogGroupEncrypted", 

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

789 ), 

790 NagSuppression( 

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

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

793 ), 

794 NagSuppression( 

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

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

797 ), 

798 # CloudWatch Alarm Action suppressions for composite alarm inputs 

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

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

801 NagSuppression( 

802 id="HIPAA.Security-CloudWatchAlarmAction", 

803 reason=( 

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

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

806 ), 

807 ), 

808 NagSuppression( 

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

810 reason=( 

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

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

813 ), 

814 ), 

815 ], 

816 ) 

817 

818 

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

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

821 acknowledge_nag_findings( 

822 stack, 

823 [ 

824 # EFS backup suppressions 

825 NagSuppression( 

826 id="HIPAA.Security-EFSInBackupPlan", 

827 reason=( 

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

829 "Default deployment prioritizes cost optimization." 

830 ), 

831 ), 

832 NagSuppression( 

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

834 reason=( 

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

836 "Default deployment prioritizes cost optimization." 

837 ), 

838 ), 

839 # CloudWatch Log Group encryption 

840 NagSuppression( 

841 id="HIPAA.Security-CloudWatchLogGroupEncrypted", 

842 reason=( 

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

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

845 ), 

846 ), 

847 NagSuppression( 

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

849 reason=( 

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

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

852 ), 

853 ), 

854 NagSuppression( 

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

856 reason=( 

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

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

859 ), 

860 ), 

861 ], 

862 ) 

863 

864 

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

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

867 acknowledge_nag_findings( 

868 stack, 

869 [ 

870 NagSuppression( 

871 id="AwsSolutions-SQS4", 

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

873 ), 

874 NagSuppression( 

875 id="Serverless-SQSRedrivePolicy", 

876 reason=( 

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

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

879 ), 

880 ), 

881 ], 

882 ) 

883 

884 

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

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

887 acknowledge_nag_findings( 

888 stack, 

889 [ 

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

891 NagSuppression( 

892 id="HIPAA.Security-SecretsManagerUsingKMSKey", 

893 reason=( 

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

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

896 ), 

897 ), 

898 NagSuppression( 

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

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

901 ), 

902 NagSuppression( 

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

904 reason=( 

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

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

907 ), 

908 ), 

909 ], 

910 ) 

911 

912 

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

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

915 acknowledge_nag_findings( 

916 stack, 

917 [ 

918 NagSuppression( 

919 id="AwsSolutions-EKS1", 

920 reason=( 

921 "The production default is a private EKS API endpoint. When operators " 

922 "explicitly opt into PUBLIC_AND_PRIVATE for development access, the " 

923 "public endpoint is authenticated and authorized through IAM." 

924 ), 

925 ), 

926 ], 

927 ) 

928 

929 

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

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

932 acknowledge_nag_findings( 

933 stack, 

934 [ 

935 NagSuppression( 

936 id="AwsSolutions-IAM4", 

937 reason=( 

938 "AWS Backup requires the AWSBackupServiceRolePolicyForBackup managed policy " 

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

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

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

942 ), 

943 applies_to=[ 

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

945 ], 

946 ), 

947 ], 

948 ) 

949 

950 

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

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

953 

954 Aurora Serverless v2 with pgvector triggers several compliance findings 

955 that are intentionally accepted for this deployment pattern. 

956 """ 

957 acknowledge_nag_findings( 

958 stack, 

959 [ 

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

961 NagSuppression( 

962 id="HIPAA.Security-SecretsManagerUsingKMSKey", 

963 reason=( 

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

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

966 ), 

967 ), 

968 NagSuppression( 

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

970 reason=( 

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

972 "AWS-managed keys by default." 

973 ), 

974 ), 

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

976 NagSuppression( 

977 id="HIPAA.Security-SecretsManagerRotationEnabled", 

978 reason=( 

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

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

981 ), 

982 ), 

983 NagSuppression( 

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

985 reason=( 

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

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

988 ), 

989 ), 

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

991 NagSuppression( 

992 id="HIPAA.Security-RDSInBackupPlan", 

993 reason=( 

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

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

996 ), 

997 ), 

998 NagSuppression( 

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

1000 reason=( 

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

1002 "recovery. AWS Backup integration is optional." 

1003 ), 

1004 ), 

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

1006 # but some frameworks check for additional log types 

1007 NagSuppression( 

1008 id="HIPAA.Security-RDSLoggingEnabled", 

1009 reason=( 

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

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

1012 ), 

1013 ), 

1014 NagSuppression( 

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

1016 reason=( 

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

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

1019 ), 

1020 ), 

1021 NagSuppression( 

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

1023 reason=( 

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

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

1026 ), 

1027 ), 

1028 # CloudWatch Log Group encryption for Aurora logs 

1029 NagSuppression( 

1030 id="HIPAA.Security-CloudWatchLogGroupEncrypted", 

1031 reason=( 

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

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

1034 ), 

1035 ), 

1036 NagSuppression( 

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

1038 reason=( 

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

1040 "AWS-managed keys." 

1041 ), 

1042 ), 

1043 NagSuppression( 

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

1045 reason=( 

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

1047 "AWS-managed keys." 

1048 ), 

1049 ), 

1050 # Enhanced monitoring IAM role uses AWS managed policy 

1051 NagSuppression( 

1052 id="AwsSolutions-IAM4", 

1053 reason=( 

1054 "Aurora enhanced monitoring requires the AWS managed policy " 

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

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

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

1058 ), 

1059 applies_to=[ 

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

1061 ], 

1062 ), 

1063 ], 

1064 ) 

1065 

1066 

1067def add_sagemaker_suppressions( 

1068 stack: Stack, 

1069 api_gateway_region: str | None = None, 

1070 global_region: str | None = None, 

1071 project_name: str = "gco", 

1072) -> None: 

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

1074 

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

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

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

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

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

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

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

1082 the documented escape hatches. 

1083 

1084 Args: 

1085 stack: The analytics stack to apply suppressions to. 

1086 api_gateway_region: Concrete region where the API Gateway stack 

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

1088 global_region: Concrete global region (used to resolve the 

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

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

1091 key lives in a different stack). 

1092 """ 

1093 api_region = api_gateway_region or "*" 

1094 gbl_region = global_region or "*" 

1095 

1096 applies_to: list[str] = [ 

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

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

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

1100 f"Resource::arn:<AWS::Partition>:sqs:*:<AWS::AccountId>:{project_name}-jobs-*", 

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

1102 # Cluster_Shared_Bucket metadata parameters under 

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

1104 # wildcard covers exactly three literal parameter names 

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

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

1107 f"Resource::arn:<AWS::Partition>:ssm:{gbl_region}:<AWS::AccountId>:parameter/{project_name}/cluster-shared-bucket/*", 

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

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

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

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

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

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

1114 # templates, and manage inference endpoints in addition to 

1115 # read-only GETs. 

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

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

1118 # KMS decrypt scoped by 

1119 # ``kms:ViaService=s3.<global-region>.<AWS::URLSuffix>`` 

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

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

1122 # documented pattern, narrowed by the ViaService condition. 

1123 "Resource::*", 

1124 # S3 grant_read_write on Studio_Only_Bucket produces the AWS- 

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

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

1127 "Action::s3:Abort*", 

1128 "Action::s3:DeleteObject*", 

1129 "Action::s3:GetBucket*", 

1130 "Action::s3:GetObject*", 

1131 "Action::s3:List*", 

1132 # KMS grant_encrypt_decrypt on Analytics_KMS_Key produces the 

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

1134 # single key ARN. 

1135 "Action::kms:GenerateDataKey*", 

1136 "Action::kms:ReEncrypt*", 

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

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

1139 "Resource::<StudioOnlyBucket80FF5E65.Arn>/*", 

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

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

1142 # service in the global region. 

1143 f"Condition::kms:ViaService:s3.{gbl_region}.<AWS::URLSuffix>", 

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

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

1146 # render the IDE and manage notebook apps. 

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

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

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

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

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

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

1153 # support resource-level scoping on most actions. 

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

1155 # SageMaker MLflow tracking servers + MLflow Apps. The 

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

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

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

1159 # ``AmazonSageMakerFullAccess`` policy covers the 

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

1161 # Tracking Servers, Model Registry) but NOT the 

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

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

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

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

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

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

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

1169 "Action::sts:GetCallerIdentity", 

1170 ] 

1171 

1172 acknowledge_nag_findings( 

1173 stack, 

1174 [ 

1175 NagSuppression( 

1176 id="AwsSolutions-IAM5", 

1177 reason=( 

1178 "SageMaker_Execution_Role uses wildcard ARNs and actions for: " 

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

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

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

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

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

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

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

1186 "scoped by kms:ViaService=s3.<global-region>.<AWS::URLSuffix> " 

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

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

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

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

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

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

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

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

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

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

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

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

1199 "(covers exactly three literal parameter names — " 

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

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

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

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

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

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

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

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

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

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

1210 "``AmazonSageMakerFullAccess`` managed policy instead " 

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

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

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

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

1215 "narrow literal pattern." 

1216 ), 

1217 applies_to=applies_to, 

1218 ), 

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

1220 # the role through Cognito-gated presigned URLs rather 

1221 # than direct AssumeRole calls from operator terminals. 

1222 NagSuppression( 

1223 id="AwsSolutions-IAM4", 

1224 reason=( 

1225 "SageMaker_Execution_Role does not attach AWS managed " 

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

1227 "and used exclusively by notebooks running inside the " 

1228 "Studio domain." 

1229 ), 

1230 ), 

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

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

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

1234 # are suppressed because this deployment intentionally uses 

1235 # the stock AWS-published SageMaker Distribution images. 

1236 NagSuppression( 

1237 id="AwsSolutions-SM2", 

1238 reason=( 

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

1240 "Distribution images and does not define custom " 

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

1242 "give POSIX isolation without a custom image." 

1243 ), 

1244 ), 

1245 NagSuppression( 

1246 id="AwsSolutions-SM3", 

1247 reason=( 

1248 "SageMaker Studio domain is provisioned with " 

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

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

1251 "Direct internet access is structurally unavailable." 

1252 ), 

1253 ), 

1254 ], 

1255 ) 

1256 

1257 # The separate SagemakerClusterSharedBucketGrant inline Policy (a 

1258 # sibling construct to the role, created by 

1259 # ``_grant_sagemaker_role_on_cluster_shared_bucket``) has its own 

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

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

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

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

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

1265 acknowledge_nag_findings( 

1266 stack, 

1267 [ 

1268 NagSuppression( 

1269 id="AwsSolutions-IAM5", 

1270 reason=( 

1271 "SagemakerClusterSharedBucketGrant attaches the RW " 

1272 "policy on the single literal Cluster_Shared_Bucket " 

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

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

1275 "object key inside the single always-on cluster-shared " 

1276 "bucket (CloudFormation-generated name, published via SSM), " 

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

1278 "analogous job-pod grant." 

1279 ), 

1280 applies_to=[ 

1281 "Resource::<ReadClusterSharedBucketArn4B0BD291.Parameter.Value>/*", 

1282 ], 

1283 ), 

1284 ], 

1285 ) 

1286 

1287 

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

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

1290 

1291 Most Cognito-related checks are handled by 

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

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

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

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

1296 there is no hosted UI callback to harden. 

1297 """ 

1298 acknowledge_nag_findings( 

1299 stack, 

1300 [ 

1301 NagSuppression( 

1302 id="AwsSolutions-COG3", 

1303 reason=( 

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

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

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

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

1308 "when required." 

1309 ), 

1310 ), 

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

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

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

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

1315 # level would lock out admins bootstrapping the first user 

1316 # during initial deploy. 

1317 NagSuppression( 

1318 id="AwsSolutions-COG2", 

1319 reason=( 

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

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

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

1323 "provides adaptive risk-based authentication that " 

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

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

1326 "admin bootstrapping user during initial deploy." 

1327 ), 

1328 ), 

1329 ], 

1330 ) 

1331 

1332 

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

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

1335 

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

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

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

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

1340 """ 

1341 acknowledge_nag_findings( 

1342 stack, 

1343 [ 

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

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

1346 # service endpoints already emit CloudTrail data events that 

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

1348 NagSuppression( 

1349 id="AwsSolutions-VPC7", 

1350 reason=( 

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

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

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

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

1355 "Flow logs would duplicate that telemetry at " 

1356 "significant storage cost without adding visibility." 

1357 ), 

1358 ), 

1359 NagSuppression( 

1360 id="HIPAA.Security-VPCFlowLogsEnabled", 

1361 reason=( 

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

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

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

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

1366 "Flow logs would duplicate that telemetry at " 

1367 "significant storage cost without adding visibility." 

1368 ), 

1369 ), 

1370 NagSuppression( 

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

1372 reason=( 

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

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

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

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

1377 "Flow logs would duplicate that telemetry at " 

1378 "significant storage cost without adding visibility." 

1379 ), 

1380 ), 

1381 NagSuppression( 

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

1383 reason=( 

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

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

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

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

1388 "Flow logs would duplicate that telemetry at " 

1389 "significant storage cost without adding visibility." 

1390 ), 

1391 ), 

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

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

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

1395 NagSuppression( 

1396 id="HIPAA.Security-VPCSubnetAutoAssignPublicIpDisabled", 

1397 reason=( 

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

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

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

1401 "in this subnet." 

1402 ), 

1403 ), 

1404 NagSuppression( 

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

1406 reason=( 

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

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

1409 ), 

1410 ), 

1411 NagSuppression( 

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

1413 reason=( 

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

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

1416 ), 

1417 ), 

1418 NagSuppression( 

1419 id="HIPAA.Security-VPCNoUnrestrictedRouteToIGW", 

1420 reason=( 

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

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

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

1424 ), 

1425 ), 

1426 NagSuppression( 

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

1428 reason=( 

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

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

1431 ), 

1432 ), 

1433 NagSuppression( 

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

1435 reason=( 

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

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

1438 ), 

1439 ), 

1440 ], 

1441 ) 

1442 

1443 

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

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

1446 

1447 The analytics stack owns two buckets: 

1448 

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

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

1451 enabled because this bucket is the endpoint of the SageMaker 

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

1453 introduce eventual-consistency behavior that breaks notebook 

1454 save/load semantics. 

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

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

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

1458 construct does not wire automatically. Replication is not enabled 

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

1460 """ 

1461 acknowledge_nag_findings( 

1462 stack, 

1463 [ 

1464 # S3 replication suppressions — both buckets are single- 

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

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

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

1468 # applicable to either. 

1469 NagSuppression( 

1470 id="HIPAA.Security-S3BucketReplicationEnabled", 

1471 reason=( 

1472 "Studio_Only_Bucket and its access-logs bucket are " 

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

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

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

1476 "cost without a corresponding availability gain (the " 

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

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

1479 "the data bucket by construction." 

1480 ), 

1481 ), 

1482 NagSuppression( 

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

1484 reason=( 

1485 "Studio_Only_Bucket and its access-logs bucket are " 

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

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

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

1489 "cost without a corresponding availability gain (the " 

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

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

1492 "the data bucket by construction." 

1493 ), 

1494 ), 

1495 NagSuppression( 

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

1497 reason=( 

1498 "Studio_Only_Bucket and its access-logs bucket are " 

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

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

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

1502 "cost without a corresponding availability gain (the " 

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

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

1505 "the data bucket by construction." 

1506 ), 

1507 ), 

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

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

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

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

1512 # not wire automatically. 

1513 NagSuppression( 

1514 id="HIPAA.Security-S3DefaultEncryptionKMS", 

1515 reason=( 

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

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

1518 "bucket requires an additional log-delivery role " 

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

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

1521 "with ``Analytics_KMS_Key``." 

1522 ), 

1523 ), 

1524 NagSuppression( 

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

1526 reason=( 

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

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

1529 "bucket requires an additional log-delivery role " 

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

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

1532 "with ``Analytics_KMS_Key``." 

1533 ), 

1534 ), 

1535 NagSuppression( 

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

1537 reason=( 

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

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

1540 "bucket requires an additional log-delivery role " 

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

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

1543 "with ``Analytics_KMS_Key``." 

1544 ), 

1545 ), 

1546 ], 

1547 ) 

1548 

1549 

1550def add_presigned_url_lambda_suppressions( 

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

1552) -> None: 

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

1554 

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

1556 ARNs because ``CreatePresignedDomainUrl``, ``DescribeUserProfile``, 

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

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

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

1560 ARN shapes we can bind in the IAM policy. 

1561 """ 

1562 region = api_gateway_region or "*" 

1563 acknowledge_nag_findings( 

1564 stack, 

1565 [ 

1566 NagSuppression( 

1567 id="AwsSolutions-IAM5", 

1568 reason=( 

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

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

1571 "because DomainId and UserProfileName are only " 

1572 "resolvable at invoke time from the incoming Cognito " 

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

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

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

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

1577 "a single paginated list call per Lambda invocation " 

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

1579 "the api-gateway region." 

1580 ), 

1581 applies_to=[ 

1582 "Resource::*", 

1583 ( 

1584 f"Resource::arn:<AWS::Partition>:sagemaker:{region}:<AWS::AccountId>:domain/*" 

1585 ), 

1586 ( 

1587 f"Resource::arn:<AWS::Partition>:sagemaker:{region}:<AWS::AccountId>:user-profile/*/*" 

1588 ), 

1589 # Generic shapes — catch tokenized-region variants 

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

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

1592 ( 

1593 "Resource::arn:<AWS::Partition>:sagemaker:<AWS::Region>:<AWS::AccountId>:domain/*" 

1594 ), 

1595 ( 

1596 "Resource::arn:<AWS::Partition>:sagemaker:<AWS::Region>:<AWS::AccountId>:user-profile/*/*" 

1597 ), 

1598 ], 

1599 ), 

1600 ], 

1601 ) 

1602 

1603 

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

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

1606 

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

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

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

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

1611 ``gco.stacks.constants``). 

1612 """ 

1613 acknowledge_nag_findings( 

1614 stack, 

1615 [ 

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

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

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

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

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

1621 # here without touching the branch dispatch. 

1622 NagSuppression( 

1623 id="AwsSolutions-EMR1", 

1624 reason=( 

1625 "EMR Serverless application is created with explicit " 

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

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

1628 ), 

1629 ), 

1630 ], 

1631 ) 

1632 

1633 

1634def apply_all_suppressions( 

1635 stack: Stack, 

1636 stack_type: str = "regional", 

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

1638 global_region: str | None = None, 

1639 api_gateway_region: str | None = None, 

1640 project_name: str = "gco", 

1641) -> None: 

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

1643 

1644 Args: 

1645 stack: The CDK stack to apply suppressions to 

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

1647 'regional_api_gateway', 'monitoring', or 'analytics' 

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

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

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

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

1652 project_name: Deployment prefix (#139), forwarded to 

1653 ``add_iam_suppressions`` so the IAM allow-list ARN patterns match 

1654 the deployment's project-scoped resource names. Defaults to 

1655 ``"gco"``. 

1656 """ 

1657 # Common suppressions for all stacks 

1658 add_lambda_suppressions(stack) 

1659 add_iam_suppressions( 

1660 stack, 

1661 regions=regions, 

1662 global_region=global_region, 

1663 project_name=project_name, 

1664 ) 

1665 

1666 if stack_type == "regional": 

1667 add_eks_suppressions(stack) 

1668 add_eks_cluster_suppressions(stack) 

1669 add_vpc_suppressions(stack) 

1670 add_storage_suppressions(stack) 

1671 add_sqs_suppressions(stack) 

1672 add_aurora_pgvector_suppressions(stack) 

1673 

1674 elif stack_type == "global": 

1675 add_backup_suppressions(stack) 

1676 

1677 elif stack_type == "api_gateway": 

1678 add_api_gateway_suppressions(stack) 

1679 add_secrets_suppressions(stack) 

1680 

1681 elif stack_type == "regional_api_gateway": 

1682 add_api_gateway_suppressions(stack) 

1683 

1684 elif stack_type == "monitoring": 

1685 add_monitoring_suppressions(stack) 

1686 

1687 elif stack_type == "analytics": 

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

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

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

1691 add_storage_suppressions(stack) 

1692 add_sagemaker_suppressions( 

1693 stack, 

1694 api_gateway_region=api_gateway_region, 

1695 global_region=global_region, 

1696 project_name=project_name, 

1697 ) 

1698 add_cognito_suppressions(stack) 

1699 add_emr_serverless_suppressions(stack) 

1700 add_analytics_vpc_suppressions(stack) 

1701 add_analytics_s3_suppressions(stack) 

1702 add_presigned_url_lambda_suppressions(stack, api_gateway_region=api_gateway_region)