@aws/nx-plugin 1.0.0-rc.51 → 1.0.0-rc.52

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/migrations.json CHANGED
@@ -16,6 +16,11 @@
16
16
  "version": "1.0.0-rc.50",
17
17
  "description": "Adopt an existing Terraform state bucket in the vended bootstrap script when its state object is missing",
18
18
  "implementation": "./src/migrations/latest/terraform-bootstrap-adopt-existing-bucket/migration"
19
+ },
20
+ "latest-user-identity-waf-allow-localhost-callback": {
21
+ "version": "1.0.0-rc.52",
22
+ "description": "Count the EC2MetaDataSSRF_QUERYARGUMENTS WAF rule on the UserIdentity Web ACL so sign-in from a local dev server is not blocked",
23
+ "implementation": "./src/migrations/latest/user-identity-waf-allow-localhost-callback/migration"
19
24
  }
20
25
  }
21
26
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aws/nx-plugin",
3
- "version": "1.0.0-rc.51",
3
+ "version": "1.0.0-rc.52",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/awslabs/nx-plugin-for-aws.git",
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+ import type { MigrationReturnObject, Tree } from '@nx/devkit';
6
+ export default function migration(tree: Tree): Promise<MigrationReturnObject>;
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */ import { applyGritQL, matchGritQL } from "../../../utils/ast.js";
5
+ import { formatFilesInSubtree } from "../../../utils/format.js";
6
+ import { PACKAGES_DIR, SHARED_CONSTRUCTS_DIR, SHARED_TERRAFORM_DIR } from "../../../utils/shared-constructs-constants.js";
7
+ /**
8
+ * Count the EC2MetaDataSSRF_QUERYARGUMENTS WAF rule on the UserIdentity Web ACL
9
+ *
10
+ * `AWSManagedRulesCommonRuleSet` treats the loopback `redirect_uri` the Cognito
11
+ * Hosted UI receives during local sign-in as an SSRF attempt, so sign-in against
12
+ * a local dev server fails with an opaque 403 before the login page renders. The
13
+ * rule is overridden to Count while a local callback URL is allowed; every other
14
+ * rule in the group still blocks.
15
+ *
16
+ * The local callback URLs are lifted into a named constant so the override can be
17
+ * derived from them rather than restating the condition.
18
+ *
19
+ * How to write a migration:
20
+ * - https://nx.dev/docs/kb/migration-generators
21
+ * - What `nextSteps` means: https://nx.dev/docs/reference/devkit/MigrationReturnObject
22
+ *
23
+ * Guardrails:
24
+ * - Pattern-match before writing: skip files that have diverged from the shape
25
+ * your generators produce and report them via `nextSteps`, rather than
26
+ * clobbering the user's changes.
27
+ * - Idempotent: re-running must be a no-op.
28
+ * - Format what you write: finish with `formatFilesInSubtree` so the files your
29
+ * migration wrote are formatted correctly.
30
+ */ const CDK_USER_IDENTITY_FILE = `${PACKAGES_DIR}/${SHARED_CONSTRUCTS_DIR}/src/core/user-identity.ts`;
31
+ const TERRAFORM_IDENTITY_FILE = `${PACKAGES_DIR}/${SHARED_TERRAFORM_DIR}/src/core/user-identity/identity/identity.tf`;
32
+ const SSRF_RULE_NAME = 'EC2MetaDataSSRF_QUERYARGUMENTS';
33
+ // Guards each file: present only once the override has been added.
34
+ const CDK_MIGRATED_PATTERN = `\`ruleActionOverrides: allowsLocalCallback ? $_ : undefined\``;
35
+ const TERRAFORM_MIGRATED_PATTERN = `language hcl\n\`name = "${SSRF_RULE_NAME}"\``;
36
+ // Every CDK edit site, matched structurally so formatting doesn't affect whether
37
+ // the construct is recognised.
38
+ const CDK_CONSTANT_ANCHOR_PATTERN = "`const WEB_CLIENT_ID = 'WebClient'`";
39
+ // Scoped to the `.concat` callsite so it can't also rewrite the array literal
40
+ // inside the constant this migration inserts.
41
+ const CDK_LOCAL_URLS_PATTERN = "`['http://localhost:4200', 'http://localhost:4300'].concat($rest)`";
42
+ const CDK_CALL_PATTERN = '`this.createWebAcl($id, this.userPool)`';
43
+ const CDK_SIGNATURE_PATTERN = '`private createWebAcl = ($id: string, $pool: UserPool) => $body`';
44
+ const CDK_STATEMENT_PATTERN = "`managedRuleGroupStatement: { name: 'AWSManagedRulesCommonRuleSet', vendorName: 'AWS' }`";
45
+ const CDK_EDITS = [
46
+ // Local callback URLs become a named constant the override can key off.
47
+ [
48
+ CDK_CONSTANT_ANCHOR_PATTERN,
49
+ `${CDK_CONSTANT_ANCHOR_PATTERN} => \`const WEB_CLIENT_ID = 'WebClient';
50
+
51
+ /** Local dev server origins permitted to complete the sign-in redirect */
52
+ const LOCAL_CALLBACK_URLS = ['http://localhost:4200', 'http://localhost:4300']\``
53
+ ],
54
+ [
55
+ CDK_LOCAL_URLS_PATTERN,
56
+ `${CDK_LOCAL_URLS_PATTERN} => \`LOCAL_CALLBACK_URLS.concat($rest)\``
57
+ ],
58
+ [
59
+ CDK_CALL_PATTERN,
60
+ `${CDK_CALL_PATTERN} => \`this.createWebAcl($id, this.userPool, LOCAL_CALLBACK_URLS.length > 0)\``
61
+ ],
62
+ [
63
+ CDK_SIGNATURE_PATTERN,
64
+ `${CDK_SIGNATURE_PATTERN} => \`private createWebAcl = (
65
+ $id: string,
66
+ $pool: UserPool,
67
+ allowsLocalCallback: boolean
68
+ ) => $body\``
69
+ ],
70
+ [
71
+ CDK_STATEMENT_PATTERN,
72
+ `${CDK_STATEMENT_PATTERN} => \`managedRuleGroupStatement: {
73
+ name: 'AWSManagedRulesCommonRuleSet',
74
+ vendorName: 'AWS',
75
+ // ${SSRF_RULE_NAME} treats the loopback redirect_uri the
76
+ // Hosted UI receives during local sign-in as an SSRF attempt. Counted
77
+ // only while a local callback URL is allowed; every other rule blocks.
78
+ ruleActionOverrides: allowsLocalCallback
79
+ ? [
80
+ {
81
+ name: '${SSRF_RULE_NAME}',
82
+ actionToUse: { count: {} },
83
+ },
84
+ ]
85
+ : undefined,
86
+ }\``
87
+ ]
88
+ ];
89
+ const TERRAFORM_DATA_SOURCES_PATTERN = [
90
+ 'language hcl',
91
+ '`data "aws_region" "current" {}`'
92
+ ].join('\n');
93
+ const TERRAFORM_EDITS = [
94
+ // Local callback URLs become a local the override can key off.
95
+ [
96
+ TERRAFORM_DATA_SOURCES_PATTERN,
97
+ [
98
+ 'language hcl',
99
+ '`data "aws_region" "current" {}` => `data "aws_region" "current" {}',
100
+ '',
101
+ 'locals {',
102
+ ' # Local dev server origins permitted to complete the sign-in redirect',
103
+ ' local_callback_urls = [',
104
+ ' "http://localhost:4200",',
105
+ ' "http://localhost:4300"',
106
+ ' ]',
107
+ '}`'
108
+ ].join('\n')
109
+ ],
110
+ [
111
+ [
112
+ 'language hcl',
113
+ '`callback_urls = concat([',
114
+ ' "http://localhost:4200",',
115
+ ' "http://localhost:4300"',
116
+ ' ], var.callback_urls)`'
117
+ ].join('\n'),
118
+ [
119
+ 'language hcl',
120
+ '`callback_urls = concat([',
121
+ ' "http://localhost:4200",',
122
+ ' "http://localhost:4300"',
123
+ ' ], var.callback_urls)` => `callback_urls = concat(local.local_callback_urls, var.callback_urls)`'
124
+ ].join('\n')
125
+ ],
126
+ [
127
+ [
128
+ 'language hcl',
129
+ '`logout_urls = concat([',
130
+ ' "http://localhost:4200",',
131
+ ' "http://localhost:4300"',
132
+ ' ], var.logout_urls)`'
133
+ ].join('\n'),
134
+ [
135
+ 'language hcl',
136
+ '`logout_urls = concat([',
137
+ ' "http://localhost:4200",',
138
+ ' "http://localhost:4300"',
139
+ ' ], var.logout_urls)` => `logout_urls = concat(local.local_callback_urls, var.logout_urls)`'
140
+ ].join('\n')
141
+ ],
142
+ [
143
+ [
144
+ 'language hcl',
145
+ '`managed_rule_group_statement {',
146
+ ' name = "AWSManagedRulesCommonRuleSet"',
147
+ ' vendor_name = "AWS"',
148
+ ' }`'
149
+ ].join('\n'),
150
+ [
151
+ 'language hcl',
152
+ '`managed_rule_group_statement {',
153
+ ' name = "AWSManagedRulesCommonRuleSet"',
154
+ ' vendor_name = "AWS"',
155
+ ' }` => `managed_rule_group_statement {',
156
+ ' name = "AWSManagedRulesCommonRuleSet"',
157
+ ' vendor_name = "AWS"',
158
+ '',
159
+ ` # ${SSRF_RULE_NAME} treats the loopback redirect_uri the`,
160
+ ' # Hosted UI receives during local sign-in as an SSRF attempt. Counted',
161
+ ' # only while a local callback URL is allowed; every other rule blocks.',
162
+ ' dynamic "rule_action_override" {',
163
+ ' for_each = length(local.local_callback_urls) > 0 ? [1] : []',
164
+ '',
165
+ ' content {',
166
+ ` name = "${SSRF_RULE_NAME}"`,
167
+ '',
168
+ ' action_to_use {',
169
+ ' count {}',
170
+ ' }',
171
+ ' }',
172
+ ' }',
173
+ ' }`'
174
+ ].join('\n')
175
+ ]
176
+ ];
177
+ const divergedNextStep = (filePath)=>`${filePath}: the UserIdentity Web ACL has diverged from the generated shape - left untouched. To sign in against a local dev server, override ${SSRF_RULE_NAME} in the AWSManagedRulesCommonRuleSet rule group to Count, otherwise the Cognito Hosted UI returns 403 for localhost redirect URIs.`;
178
+ const migratedNextStep = (filePath)=>`${filePath}: ${SSRF_RULE_NAME} is now counted rather than blocked while a local callback URL is allowed, so signing in against a local dev server works. Redeploy to apply it.`;
179
+ export default async function migration(tree) {
180
+ const nextSteps = [];
181
+ for (const [filePath, migratedPattern, edits] of [
182
+ [
183
+ CDK_USER_IDENTITY_FILE,
184
+ CDK_MIGRATED_PATTERN,
185
+ CDK_EDITS
186
+ ],
187
+ [
188
+ TERRAFORM_IDENTITY_FILE,
189
+ TERRAFORM_MIGRATED_PATTERN,
190
+ TERRAFORM_EDITS
191
+ ]
192
+ ]){
193
+ if (!tree.exists(filePath)) {
194
+ continue;
195
+ }
196
+ if (await matchGritQL(tree, filePath, migratedPattern)) {
197
+ continue;
198
+ }
199
+ // Confirm every edit site is present before writing any of them, so a file
200
+ // that only partly matches is left whole rather than half-edited.
201
+ const allSitesPresent = (await Promise.all(edits.map(([match])=>matchGritQL(tree, filePath, match)))).every(Boolean);
202
+ if (!allSitesPresent) {
203
+ nextSteps.push(divergedNextStep(filePath));
204
+ continue;
205
+ }
206
+ for (const [, rewrite] of edits){
207
+ await applyGritQL(tree, filePath, rewrite);
208
+ }
209
+ nextSteps.push(migratedNextStep(filePath));
210
+ }
211
+ await formatFilesInSubtree(tree);
212
+ return {
213
+ nextSteps
214
+ };
215
+ }
216
+
217
+ //# sourceMappingURL=migration.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../packages/nx-plugin/src/migrations/latest/user-identity-waf-allow-localhost-callback/migration.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\nimport type { MigrationReturnObject, Tree } from '@nx/devkit';\nimport { applyGritQL, matchGritQL } from '../../../utils/ast';\nimport { formatFilesInSubtree } from '../../../utils/format';\nimport {\n PACKAGES_DIR,\n SHARED_CONSTRUCTS_DIR,\n SHARED_TERRAFORM_DIR,\n} from '../../../utils/shared-constructs-constants';\n\n/**\n * Count the EC2MetaDataSSRF_QUERYARGUMENTS WAF rule on the UserIdentity Web ACL\n *\n * `AWSManagedRulesCommonRuleSet` treats the loopback `redirect_uri` the Cognito\n * Hosted UI receives during local sign-in as an SSRF attempt, so sign-in against\n * a local dev server fails with an opaque 403 before the login page renders. The\n * rule is overridden to Count while a local callback URL is allowed; every other\n * rule in the group still blocks.\n *\n * The local callback URLs are lifted into a named constant so the override can be\n * derived from them rather than restating the condition.\n *\n * How to write a migration:\n * - https://nx.dev/docs/kb/migration-generators\n * - What `nextSteps` means: https://nx.dev/docs/reference/devkit/MigrationReturnObject\n *\n * Guardrails:\n * - Pattern-match before writing: skip files that have diverged from the shape\n * your generators produce and report them via `nextSteps`, rather than\n * clobbering the user's changes.\n * - Idempotent: re-running must be a no-op.\n * - Format what you write: finish with `formatFilesInSubtree` so the files your\n * migration wrote are formatted correctly.\n */\n\nconst CDK_USER_IDENTITY_FILE = `${PACKAGES_DIR}/${SHARED_CONSTRUCTS_DIR}/src/core/user-identity.ts`;\nconst TERRAFORM_IDENTITY_FILE = `${PACKAGES_DIR}/${SHARED_TERRAFORM_DIR}/src/core/user-identity/identity/identity.tf`;\n\nconst SSRF_RULE_NAME = 'EC2MetaDataSSRF_QUERYARGUMENTS';\n\n// Guards each file: present only once the override has been added.\nconst CDK_MIGRATED_PATTERN = `\\`ruleActionOverrides: allowsLocalCallback ? $_ : undefined\\``;\nconst TERRAFORM_MIGRATED_PATTERN = `language hcl\\n\\`name = \"${SSRF_RULE_NAME}\"\\``;\n\n// Every CDK edit site, matched structurally so formatting doesn't affect whether\n// the construct is recognised.\nconst CDK_CONSTANT_ANCHOR_PATTERN = \"`const WEB_CLIENT_ID = 'WebClient'`\";\n// Scoped to the `.concat` callsite so it can't also rewrite the array literal\n// inside the constant this migration inserts.\nconst CDK_LOCAL_URLS_PATTERN =\n \"`['http://localhost:4200', 'http://localhost:4300'].concat($rest)`\";\nconst CDK_CALL_PATTERN = '`this.createWebAcl($id, this.userPool)`';\nconst CDK_SIGNATURE_PATTERN =\n '`private createWebAcl = ($id: string, $pool: UserPool) => $body`';\nconst CDK_STATEMENT_PATTERN =\n \"`managedRuleGroupStatement: { name: 'AWSManagedRulesCommonRuleSet', vendorName: 'AWS' }`\";\n\nconst CDK_EDITS: Array<[string, string]> = [\n // Local callback URLs become a named constant the override can key off.\n [\n CDK_CONSTANT_ANCHOR_PATTERN,\n `${CDK_CONSTANT_ANCHOR_PATTERN} => \\`const WEB_CLIENT_ID = 'WebClient';\n\n/** Local dev server origins permitted to complete the sign-in redirect */\nconst LOCAL_CALLBACK_URLS = ['http://localhost:4200', 'http://localhost:4300']\\``,\n ],\n [\n CDK_LOCAL_URLS_PATTERN,\n `${CDK_LOCAL_URLS_PATTERN} => \\`LOCAL_CALLBACK_URLS.concat($rest)\\``,\n ],\n [\n CDK_CALL_PATTERN,\n `${CDK_CALL_PATTERN} => \\`this.createWebAcl($id, this.userPool, LOCAL_CALLBACK_URLS.length > 0)\\``,\n ],\n [\n CDK_SIGNATURE_PATTERN,\n `${CDK_SIGNATURE_PATTERN} => \\`private createWebAcl = (\n $id: string,\n $pool: UserPool,\n allowsLocalCallback: boolean\n ) => $body\\``,\n ],\n [\n CDK_STATEMENT_PATTERN,\n `${CDK_STATEMENT_PATTERN} => \\`managedRuleGroupStatement: {\n name: 'AWSManagedRulesCommonRuleSet',\n vendorName: 'AWS',\n // ${SSRF_RULE_NAME} treats the loopback redirect_uri the\n // Hosted UI receives during local sign-in as an SSRF attempt. Counted\n // only while a local callback URL is allowed; every other rule blocks.\n ruleActionOverrides: allowsLocalCallback\n ? [\n {\n name: '${SSRF_RULE_NAME}',\n actionToUse: { count: {} },\n },\n ]\n : undefined,\n }\\``,\n ],\n];\n\nconst TERRAFORM_DATA_SOURCES_PATTERN = [\n 'language hcl',\n '`data \"aws_region\" \"current\" {}`',\n].join('\\n');\n\nconst TERRAFORM_EDITS: Array<[string, string]> = [\n // Local callback URLs become a local the override can key off.\n [\n TERRAFORM_DATA_SOURCES_PATTERN,\n [\n 'language hcl',\n '`data \"aws_region\" \"current\" {}` => `data \"aws_region\" \"current\" {}',\n '',\n 'locals {',\n ' # Local dev server origins permitted to complete the sign-in redirect',\n ' local_callback_urls = [',\n ' \"http://localhost:4200\",',\n ' \"http://localhost:4300\"',\n ' ]',\n '}`',\n ].join('\\n'),\n ],\n [\n [\n 'language hcl',\n '`callback_urls = concat([',\n ' \"http://localhost:4200\",',\n ' \"http://localhost:4300\"',\n ' ], var.callback_urls)`',\n ].join('\\n'),\n [\n 'language hcl',\n '`callback_urls = concat([',\n ' \"http://localhost:4200\",',\n ' \"http://localhost:4300\"',\n ' ], var.callback_urls)` => `callback_urls = concat(local.local_callback_urls, var.callback_urls)`',\n ].join('\\n'),\n ],\n [\n [\n 'language hcl',\n '`logout_urls = concat([',\n ' \"http://localhost:4200\",',\n ' \"http://localhost:4300\"',\n ' ], var.logout_urls)`',\n ].join('\\n'),\n [\n 'language hcl',\n '`logout_urls = concat([',\n ' \"http://localhost:4200\",',\n ' \"http://localhost:4300\"',\n ' ], var.logout_urls)` => `logout_urls = concat(local.local_callback_urls, var.logout_urls)`',\n ].join('\\n'),\n ],\n [\n [\n 'language hcl',\n '`managed_rule_group_statement {',\n ' name = \"AWSManagedRulesCommonRuleSet\"',\n ' vendor_name = \"AWS\"',\n ' }`',\n ].join('\\n'),\n [\n 'language hcl',\n '`managed_rule_group_statement {',\n ' name = \"AWSManagedRulesCommonRuleSet\"',\n ' vendor_name = \"AWS\"',\n ' }` => `managed_rule_group_statement {',\n ' name = \"AWSManagedRulesCommonRuleSet\"',\n ' vendor_name = \"AWS\"',\n '',\n ` # ${SSRF_RULE_NAME} treats the loopback redirect_uri the`,\n ' # Hosted UI receives during local sign-in as an SSRF attempt. Counted',\n ' # only while a local callback URL is allowed; every other rule blocks.',\n ' dynamic \"rule_action_override\" {',\n ' for_each = length(local.local_callback_urls) > 0 ? [1] : []',\n '',\n ' content {',\n ` name = \"${SSRF_RULE_NAME}\"`,\n '',\n ' action_to_use {',\n ' count {}',\n ' }',\n ' }',\n ' }',\n ' }`',\n ].join('\\n'),\n ],\n];\n\nconst divergedNextStep = (filePath: string) =>\n `${filePath}: the UserIdentity Web ACL has diverged from the generated shape - left untouched. To sign in against a local dev server, override ${SSRF_RULE_NAME} in the AWSManagedRulesCommonRuleSet rule group to Count, otherwise the Cognito Hosted UI returns 403 for localhost redirect URIs.`;\n\nconst migratedNextStep = (filePath: string) =>\n `${filePath}: ${SSRF_RULE_NAME} is now counted rather than blocked while a local callback URL is allowed, so signing in against a local dev server works. Redeploy to apply it.`;\n\nexport default async function migration(\n tree: Tree,\n): Promise<MigrationReturnObject> {\n const nextSteps: string[] = [];\n\n for (const [filePath, migratedPattern, edits] of [\n [CDK_USER_IDENTITY_FILE, CDK_MIGRATED_PATTERN, CDK_EDITS],\n [TERRAFORM_IDENTITY_FILE, TERRAFORM_MIGRATED_PATTERN, TERRAFORM_EDITS],\n ] as const) {\n if (!tree.exists(filePath)) {\n // This workspace doesn't use this IaC provider, or has no UserIdentity.\n continue;\n }\n\n if (await matchGritQL(tree, filePath, migratedPattern)) {\n // Already migrated - silent skip keeps re-runs a no-op.\n continue;\n }\n\n // Confirm every edit site is present before writing any of them, so a file\n // that only partly matches is left whole rather than half-edited.\n const allSitesPresent = (\n await Promise.all(\n edits.map(([match]) => matchGritQL(tree, filePath, match)),\n )\n ).every(Boolean);\n\n if (!allSitesPresent) {\n nextSteps.push(divergedNextStep(filePath));\n continue;\n }\n\n for (const [, rewrite] of edits) {\n await applyGritQL(tree, filePath, rewrite);\n }\n nextSteps.push(migratedNextStep(filePath));\n }\n\n await formatFilesInSubtree(tree);\n\n return { nextSteps };\n}\n"],"names":["applyGritQL","matchGritQL","formatFilesInSubtree","PACKAGES_DIR","SHARED_CONSTRUCTS_DIR","SHARED_TERRAFORM_DIR","CDK_USER_IDENTITY_FILE","TERRAFORM_IDENTITY_FILE","SSRF_RULE_NAME","CDK_MIGRATED_PATTERN","TERRAFORM_MIGRATED_PATTERN","CDK_CONSTANT_ANCHOR_PATTERN","CDK_LOCAL_URLS_PATTERN","CDK_CALL_PATTERN","CDK_SIGNATURE_PATTERN","CDK_STATEMENT_PATTERN","CDK_EDITS","TERRAFORM_DATA_SOURCES_PATTERN","join","TERRAFORM_EDITS","divergedNextStep","filePath","migratedNextStep","migration","tree","nextSteps","migratedPattern","edits","exists","allSitesPresent","Promise","all","map","match","every","Boolean","push","rewrite"],"mappings":"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"}
@@ -125,6 +125,9 @@ import { suppressRules } from './checkov.js';
125
125
 
126
126
  const WEB_CLIENT_ID = 'WebClient';
127
127
 
128
+ /** Local dev server origins permitted to complete the sign-in redirect */
129
+ const LOCAL_CALLBACK_URLS = ['http://localhost:4200', 'http://localhost:4300'];
130
+
128
131
  export interface UserIdentityProps {
129
132
  /**
130
133
  * Whether to enable AWS WAFv2 with the default managed ruleset
@@ -160,7 +163,11 @@ export class UserIdentity extends Construct {
160
163
  this.userPool = this.createUserPool();
161
164
 
162
165
  if (enableWaf) {
163
- this.webAcl = this.createWebAcl(id, this.userPool);
166
+ this.webAcl = this.createWebAcl(
167
+ id,
168
+ this.userPool,
169
+ LOCAL_CALLBACK_URLS.length > 0,
170
+ );
164
171
  }
165
172
  this.userPoolDomain = this.createUserPoolDomain(this.userPool);
166
173
  this.userPoolClient = this.createUserPoolClient(this.userPool);
@@ -248,7 +255,11 @@ export class UserIdentity extends Construct {
248
255
  return userPool;
249
256
  };
250
257
 
251
- private createWebAcl = (id: string, userPool: UserPool) => {
258
+ private createWebAcl = (
259
+ id: string,
260
+ userPool: UserPool,
261
+ allowsLocalCallback: boolean,
262
+ ) => {
252
263
  const webAcl = new CfnWebACL(this, 'WebAcl', {
253
264
  defaultAction: { allow: {} },
254
265
  scope: 'REGIONAL',
@@ -265,6 +276,17 @@ export class UserIdentity extends Construct {
265
276
  managedRuleGroupStatement: {
266
277
  name: 'AWSManagedRulesCommonRuleSet',
267
278
  vendorName: 'AWS',
279
+ // EC2MetaDataSSRF_QUERYARGUMENTS treats the loopback redirect_uri the
280
+ // Hosted UI receives during local sign-in as an SSRF attempt. Counted
281
+ // only while a local callback URL is allowed; every other rule blocks.
282
+ ruleActionOverrides: allowsLocalCallback
283
+ ? [
284
+ {
285
+ name: 'EC2MetaDataSSRF_QUERYARGUMENTS',
286
+ actionToUse: { count: {} },
287
+ },
288
+ ]
289
+ : undefined,
268
290
  },
269
291
  },
270
292
  visibilityConfig: {
@@ -335,7 +357,7 @@ export class UserIdentity extends Construct {
335
357
  private createUserPoolClient = (userPool: UserPool) => {
336
358
  const lazilyComputedCallbackUrls = Lazy.list({
337
359
  produce: () =>
338
- ['http://localhost:4200', 'http://localhost:4300'].concat(
360
+ LOCAL_CALLBACK_URLS.concat(
339
361
  Stack.of(this)
340
362
  .node.findAll()
341
363
  .filter(
@@ -36,6 +36,9 @@ import { suppressRules } from './checkov<% if (esm) { %>.js<% } %>';
36
36
 
37
37
  const WEB_CLIENT_ID = 'WebClient';
38
38
 
39
+ /** Local dev server origins permitted to complete the sign-in redirect */
40
+ const LOCAL_CALLBACK_URLS = ['http://localhost:4200', 'http://localhost:4300'];
41
+
39
42
  export interface UserIdentityProps {
40
43
  /**
41
44
  * Whether to enable AWS WAFv2 with the default managed ruleset
@@ -67,7 +70,11 @@ export class UserIdentity extends Construct {
67
70
  this.userPool = this.createUserPool();
68
71
 
69
72
  if (enableWaf) {
70
- this.webAcl = this.createWebAcl(id, this.userPool);
73
+ this.webAcl = this.createWebAcl(
74
+ id,
75
+ this.userPool,
76
+ LOCAL_CALLBACK_URLS.length > 0
77
+ );
71
78
  }
72
79
  this.userPoolDomain = this.createUserPoolDomain(this.userPool);
73
80
  this.userPoolClient = this.createUserPoolClient(this.userPool);
@@ -155,7 +162,11 @@ export class UserIdentity extends Construct {
155
162
  return userPool;
156
163
  };
157
164
 
158
- private createWebAcl = (id: string, userPool: UserPool) => {
165
+ private createWebAcl = (
166
+ id: string,
167
+ userPool: UserPool,
168
+ allowsLocalCallback: boolean
169
+ ) => {
159
170
  const webAcl = new CfnWebACL(this, 'WebAcl', {
160
171
  defaultAction: { allow: {} },
161
172
  scope: 'REGIONAL',
@@ -172,6 +183,17 @@ export class UserIdentity extends Construct {
172
183
  managedRuleGroupStatement: {
173
184
  name: 'AWSManagedRulesCommonRuleSet',
174
185
  vendorName: 'AWS',
186
+ // EC2MetaDataSSRF_QUERYARGUMENTS treats the loopback redirect_uri the
187
+ // Hosted UI receives during local sign-in as an SSRF attempt. Counted
188
+ // only while a local callback URL is allowed; every other rule blocks.
189
+ ruleActionOverrides: allowsLocalCallback
190
+ ? [
191
+ {
192
+ name: 'EC2MetaDataSSRF_QUERYARGUMENTS',
193
+ actionToUse: { count: {} },
194
+ },
195
+ ]
196
+ : undefined,
175
197
  },
176
198
  },
177
199
  visibilityConfig: {
@@ -242,7 +264,7 @@ export class UserIdentity extends Construct {
242
264
  private createUserPoolClient = (userPool: UserPool) => {
243
265
  const lazilyComputedCallbackUrls = Lazy.list({
244
266
  produce: () =>
245
- ['http://localhost:4200', 'http://localhost:4300'].concat(
267
+ LOCAL_CALLBACK_URLS.concat(
246
268
  Stack.of(this)
247
269
  .node.findAll()
248
270
  .filter((child): child is Distribution => child instanceof Distribution)
@@ -44,6 +44,14 @@ variable "logout_urls" {
44
44
  data "aws_caller_identity" "current" {}
45
45
  data "aws_region" "current" {}
46
46
 
47
+ locals {
48
+ # Local dev server origins permitted to complete the sign-in redirect
49
+ local_callback_urls = [
50
+ "http://localhost:4200",
51
+ "http://localhost:4300"
52
+ ]
53
+ }
54
+
47
55
  # Random suffix for resource names
48
56
  resource "random_id" "unique_suffix" {
49
57
  byte_length = 4
@@ -260,6 +268,21 @@ resource "aws_wafv2_web_acl" "user_pool_waf" {
260
268
  managed_rule_group_statement {
261
269
  name = "AWSManagedRulesCommonRuleSet"
262
270
  vendor_name = "AWS"
271
+
272
+ # EC2MetaDataSSRF_QUERYARGUMENTS treats the loopback redirect_uri the
273
+ # Hosted UI receives during local sign-in as an SSRF attempt. Counted
274
+ # only while a local callback URL is allowed; every other rule blocks.
275
+ dynamic "rule_action_override" {
276
+ for_each = length(local.local_callback_urls) > 0 ? [1] : []
277
+
278
+ content {
279
+ name = "EC2MetaDataSSRF_QUERYARGUMENTS"
280
+
281
+ action_to_use {
282
+ count {}
283
+ }
284
+ }
285
+ }
263
286
  }
264
287
  }
265
288
 
@@ -349,15 +372,9 @@ resource "aws_cognito_user_pool_client" "web_client" {
349
372
  allowed_oauth_scopes = ["email", "openid", "profile"]
350
373
 
351
374
  # OAuth-dependent URLs - set after OAuth configuration
352
- callback_urls = concat([
353
- "http://localhost:4200",
354
- "http://localhost:4300"
355
- ], var.callback_urls)
375
+ callback_urls = concat(local.local_callback_urls, var.callback_urls)
356
376
 
357
- logout_urls = concat([
358
- "http://localhost:4200",
359
- "http://localhost:4300"
360
- ], var.logout_urls)
377
+ logout_urls = concat(local.local_callback_urls, var.logout_urls)
361
378
 
362
379
  # Security settings
363
380
  prevent_user_existence_errors = "ENABLED"