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

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 

41from gco.stacks.constants import api_gateway_auth_secret_name 

42 

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. 

74 

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

76_ACK_METADATA_KEY: str = Validations.ACKNOWLEDGED_RULES_METADATA_KEY 

77 

78 

79@dataclass(frozen=True) 

80class NagSuppression: 

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

82 

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 """ 

95 

96 id: str 

97 reason: str 

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

99 

100 

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

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

103 

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 []) 

116 

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 

124 

125 

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). 

131 

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 ) 

161 

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] 

180 

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) 

191 

192 

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) 

209 

210 

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. 

213 

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 ) 

225 

226 

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. 

231 

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 ] 

244 

245 

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. 

253 

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. 

261 

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. 

269 

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 ) 

295 

296 

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

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

299 

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 ] 

318 

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 ) 

333 

334 

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

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

337 

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 ] 

345 

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 ) 

442 

443 

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. 

452 

453 CDK generates inline policies for custom resources and some patterns 

454 require wildcard permissions for dynamic resource access. 

455 

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" 

479 

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 ] 

504 

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 ) 

511 

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) 

522 

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 ) 

533 

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 ) 

545 

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 ) 

556 

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/*") 

559 

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 ) 

602 

603 

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

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

606 

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 ) 

666 

667 

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 ) 

772 

773 

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 ) 

836 

837 

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 ) 

882 

883 

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 ) 

902 

903 

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 ) 

930 

931 

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 ) 

947 

948 

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 ) 

968 

969 

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

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

972 

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 ) 

1084 

1085 

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. 

1093 

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. 

1102 

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 "*" 

1114 

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 ] 

1190 

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 ) 

1275 

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 ) 

1305 

1306 

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

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

1309 

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 ) 

1350 

1351 

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

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

1354 

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 ) 

1461 

1462 

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

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

1465 

1466 The analytics stack owns two buckets: 

1467 

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 ) 

1567 

1568 

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. 

1573 

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 ) 

1621 

1622 

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

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

1625 

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 ) 

1651 

1652 

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. 

1662 

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 ) 

1685 

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) 

1693 

1694 elif stack_type == "global": 

1695 add_backup_suppressions(stack) 

1696 

1697 elif stack_type == "api_gateway": 

1698 add_api_gateway_suppressions(stack) 

1699 add_secrets_suppressions(stack) 

1700 

1701 elif stack_type == "regional_api_gateway": 

1702 add_api_gateway_suppressions(stack) 

1703 

1704 elif stack_type == "monitoring": 

1705 add_monitoring_suppressions(stack) 

1706 

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)