@friggframework/devtools 2.0.0-next.39 → 2.0.0-next.40

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.
@@ -1,34 +1,49 @@
1
- let EC2Client, DescribeVpcsCommand, DescribeSubnetsCommand, DescribeSecurityGroupsCommand, DescribeRouteTablesCommand, DescribeNatGatewaysCommand, DescribeAddressesCommand;
1
+ let EC2Client,
2
+ DescribeVpcsCommand,
3
+ DescribeSubnetsCommand,
4
+ DescribeSecurityGroupsCommand,
5
+ DescribeRouteTablesCommand,
6
+ DescribeNatGatewaysCommand,
7
+ DescribeAddressesCommand,
8
+ DescribeInternetGatewaysCommand;
2
9
  let KMSClient, ListKeysCommand, DescribeKeyCommand;
3
10
  let STSClient, GetCallerIdentityCommand;
4
11
 
5
12
  function loadEC2() {
6
13
  if (!EC2Client) {
7
- ({ EC2Client, DescribeVpcsCommand, DescribeSubnetsCommand, DescribeSecurityGroupsCommand, DescribeRouteTablesCommand, DescribeNatGatewaysCommand, DescribeAddressesCommand } = require('@aws-sdk/client-ec2'));
14
+ ({
15
+ EC2Client,
16
+ DescribeVpcsCommand,
17
+ DescribeSubnetsCommand,
18
+ DescribeSecurityGroupsCommand,
19
+ DescribeRouteTablesCommand,
20
+ DescribeNatGatewaysCommand,
21
+ DescribeAddressesCommand,
22
+ DescribeInternetGatewaysCommand,
23
+ } = require('@aws-sdk/client-ec2'));
8
24
  }
9
25
  }
10
26
 
11
27
  function loadKMS() {
12
28
  if (!KMSClient) {
13
- ({ KMSClient, ListKeysCommand, DescribeKeyCommand } = require('@aws-sdk/client-kms'));
29
+ ({
30
+ KMSClient,
31
+ ListKeysCommand,
32
+ DescribeKeyCommand,
33
+ } = require('@aws-sdk/client-kms'));
14
34
  }
15
35
  }
16
36
 
17
37
  function loadSTS() {
18
38
  if (!STSClient) {
19
- ({ STSClient, GetCallerIdentityCommand } = require('@aws-sdk/client-sts'));
39
+ ({
40
+ STSClient,
41
+ GetCallerIdentityCommand,
42
+ } = require('@aws-sdk/client-sts'));
20
43
  }
21
44
  }
22
45
 
23
- /**
24
- * AWS Resource Discovery utilities for Frigg applications
25
- * These functions use AWS credentials to discover default resources during build time
26
- */
27
46
  class AWSDiscovery {
28
- /**
29
- * Creates an instance of AWSDiscovery
30
- * @param {string} [region='us-east-1'] - AWS region to use for discovery
31
- */
32
47
  constructor(region = 'us-east-1') {
33
48
  this.region = region;
34
49
  loadEC2();
@@ -39,11 +54,6 @@ class AWSDiscovery {
39
54
  this.stsClient = new STSClient({ region });
40
55
  }
41
56
 
42
- /**
43
- * Get AWS account ID
44
- * @returns {Promise<string>} The AWS account ID
45
- * @throws {Error} If unable to retrieve account ID
46
- */
47
57
  async getAccountId() {
48
58
  try {
49
59
  const command = new GetCallerIdentityCommand({});
@@ -55,37 +65,31 @@ class AWSDiscovery {
55
65
  }
56
66
  }
57
67
 
58
- /**
59
- * Find the default VPC for the account
60
- * @returns {Promise<Object>} VPC object containing VpcId and other properties
61
- * @throws {Error} If no VPC is found in the account
62
- */
63
68
  async findDefaultVpc() {
64
69
  try {
65
70
  const command = new DescribeVpcsCommand({
66
71
  Filters: [
67
72
  {
68
73
  Name: 'is-default',
69
- Values: ['true']
70
- }
71
- ]
74
+ Values: ['true'],
75
+ },
76
+ ],
72
77
  });
73
-
78
+
74
79
  const response = await this.ec2Client.send(command);
75
-
80
+
76
81
  if (response.Vpcs && response.Vpcs.length > 0) {
77
82
  return response.Vpcs[0];
78
83
  }
79
-
80
- // If no default VPC, get the first available VPC
84
+
81
85
  const allVpcsCommand = new DescribeVpcsCommand({});
82
86
  const allVpcsResponse = await this.ec2Client.send(allVpcsCommand);
83
-
87
+
84
88
  if (allVpcsResponse.Vpcs && allVpcsResponse.Vpcs.length > 0) {
85
89
  console.log('No default VPC found, using first available VPC');
86
90
  return allVpcsResponse.Vpcs[0];
87
91
  }
88
-
92
+
89
93
  throw new Error('No VPC found in the account');
90
94
  } catch (error) {
91
95
  console.error('Error finding default VPC:', error.message);
@@ -93,179 +97,94 @@ class AWSDiscovery {
93
97
  }
94
98
  }
95
99
 
96
- /**
97
- * Find private subnets for the given VPC
98
- * @param {string} vpcId - The VPC ID to search within
99
- * @returns {Promise<Array>} Array of subnet objects (at least 2 for high availability)
100
- * @throws {Error} If no subnets are found in the VPC
101
- */
102
- async findPrivateSubnets(vpcId) {
100
+ async findPrivateSubnets(vpcId, autoConvert = false) {
103
101
  try {
104
- const command = new DescribeSubnetsCommand({
105
- Filters: [
106
- {
107
- Name: 'vpc-id',
108
- Values: [vpcId]
109
- }
110
- ]
111
- });
112
-
113
- const response = await this.ec2Client.send(command);
114
-
115
- if (!response.Subnets || response.Subnets.length === 0) {
102
+ const subnets = await this._fetchSubnets(vpcId);
103
+
104
+ if (subnets.length === 0) {
116
105
  throw new Error(`No subnets found in VPC ${vpcId}`);
117
106
  }
118
107
 
119
- // Prefer private subnets (no direct route to IGW)
120
- const privateSubnets = [];
121
- const publicSubnets = [];
108
+ console.log(`\nšŸ” Analyzing ${subnets.length} subnets in VPC ${vpcId}...`);
122
109
 
123
- for (const subnet of response.Subnets) {
124
- // Check route tables to determine if subnet is private
125
- const isPrivate = await this.isSubnetPrivate(subnet.SubnetId);
126
- if (isPrivate) {
127
- privateSubnets.push(subnet);
128
- } else {
129
- publicSubnets.push(subnet);
130
- }
131
- }
110
+ const { privateSubnets, publicSubnets } = await this._classifySubnets(
111
+ subnets,
112
+ { logDetails: true }
113
+ );
114
+
115
+ this._logSubnetSummary(privateSubnets.length, publicSubnets.length);
116
+
117
+ const selection = this._selectSubnetsForLambda({
118
+ privateSubnets,
119
+ publicSubnets,
120
+ autoConvert,
121
+ vpcId,
122
+ });
132
123
 
133
- // Return at least 2 subnets for high availability
134
- const selectedSubnets = privateSubnets.length >= 2 ?
135
- privateSubnets.slice(0, 2) :
136
- response.Subnets.slice(0, 2);
124
+ if (selection) {
125
+ return selection;
126
+ }
137
127
 
138
- return selectedSubnets;
128
+ throw new Error(`No subnets found in VPC ${vpcId}`);
139
129
  } catch (error) {
140
130
  console.error('Error finding private subnets:', error);
141
131
  throw error;
142
132
  }
143
133
  }
144
134
 
145
- /**
146
- * Check if a subnet is private (no direct route to Internet Gateway)
147
- * @param {string} subnetId - The subnet ID to check
148
- * @returns {Promise<boolean>} True if subnet is private, false if public
149
- */
150
- async isSubnetPrivate(subnetId) {
135
+ async isSubnetPublic(subnetId, vpcId) {
136
+ const isPrivate = await this.isSubnetPrivate(subnetId, vpcId);
137
+ return !isPrivate;
138
+ }
139
+
140
+ async isSubnetPrivate(subnetId, vpcId) {
151
141
  try {
152
- // First, get the subnet details to find its VPC
153
- const subnetCommand = new DescribeSubnetsCommand({
154
- SubnetIds: [subnetId]
155
- });
156
- const subnetResponse = await this.ec2Client.send(subnetCommand);
157
-
158
- if (!subnetResponse.Subnets || subnetResponse.Subnets.length === 0) {
159
- throw new Error(`Subnet ${subnetId} not found`);
160
- }
161
-
162
- const subnet = subnetResponse.Subnets[0];
163
- const vpcId = subnet.VpcId;
164
-
165
- // Get all route tables for this VPC
166
- const routeTablesCommand = new DescribeRouteTablesCommand({
167
- Filters: [
168
- {
169
- Name: 'vpc-id',
170
- Values: [vpcId]
171
- }
172
- ]
173
- });
174
-
175
- const routeTablesResponse = await this.ec2Client.send(routeTablesCommand);
176
-
177
- // Find the route table for this subnet
178
- let routeTable = null;
179
-
180
- // First check for explicit association
181
- for (const rt of routeTablesResponse.RouteTables || []) {
182
- for (const assoc of rt.Associations || []) {
183
- if (assoc.SubnetId === subnetId) {
184
- routeTable = rt;
185
- break;
186
- }
187
- }
188
- if (routeTable) break;
189
- }
190
-
191
- // If no explicit association, use the main route table
192
- if (!routeTable) {
193
- for (const rt of routeTablesResponse.RouteTables || []) {
194
- for (const assoc of rt.Associations || []) {
195
- if (assoc.Main === true) {
196
- routeTable = rt;
197
- break;
198
- }
199
- }
200
- if (routeTable) break;
201
- }
202
- }
203
-
142
+ const targetVpcId = vpcId || (await this._getSubnetVpcId(subnetId));
143
+
144
+ const routeTables = await this.findRouteTables(targetVpcId);
145
+ const routeTable = this._findRouteTableForSubnet(routeTables, subnetId);
146
+
204
147
  if (!routeTable) {
205
148
  console.warn(`No route table found for subnet ${subnetId}`);
206
- return true; // Default to private for safety
149
+ return true;
207
150
  }
208
-
209
- // Check if route table has a route to an Internet Gateway
210
- for (const route of routeTable.Routes || []) {
211
- if (route.GatewayId && route.GatewayId.startsWith('igw-')) {
212
- return false; // It's a public subnet
213
- }
151
+
152
+ const gatewayId = this._findIgwRoute(routeTable);
153
+ if (gatewayId) {
154
+ console.log(
155
+ `āœ… Subnet ${subnetId} is PUBLIC (has route to IGW ${gatewayId})`
156
+ );
157
+ return false;
214
158
  }
215
-
216
- return true; // No IGW route found, it's private
159
+
160
+ console.log(
161
+ `šŸ”’ Subnet ${subnetId} is PRIVATE (no IGW route found)`
162
+ );
163
+ return true;
217
164
  } catch (error) {
218
- console.warn(`Could not determine if subnet ${subnetId} is private:`, error);
219
- return true; // Default to private for safety
165
+ console.warn(
166
+ `Could not determine if subnet ${subnetId} is private:`,
167
+ error
168
+ );
169
+ return true;
220
170
  }
221
171
  }
222
172
 
223
- /**
224
- * Find or create a default security group for Lambda functions
225
- * @param {string} vpcId - The VPC ID to search within
226
- * @returns {Promise<Object>} Security group object containing GroupId and other properties
227
- * @throws {Error} If no security group is found for the VPC
228
- */
229
173
  async findDefaultSecurityGroup(vpcId) {
230
174
  try {
231
- // First try to find existing Frigg security group
232
- const friggSgCommand = new DescribeSecurityGroupsCommand({
233
- Filters: [
234
- {
235
- Name: 'vpc-id',
236
- Values: [vpcId]
237
- },
238
- {
239
- Name: 'group-name',
240
- Values: ['frigg-lambda-sg']
241
- }
242
- ]
243
- });
244
-
245
- const friggResponse = await this.ec2Client.send(friggSgCommand);
246
- if (friggResponse.SecurityGroups && friggResponse.SecurityGroups.length > 0) {
247
- return friggResponse.SecurityGroups[0];
175
+ const friggGroup = await this._findSecurityGroupByName(
176
+ vpcId,
177
+ 'frigg-lambda-sg'
178
+ );
179
+ if (friggGroup) {
180
+ return friggGroup;
248
181
  }
249
182
 
250
- // Fall back to default security group
251
- const defaultSgCommand = new DescribeSecurityGroupsCommand({
252
- Filters: [
253
- {
254
- Name: 'vpc-id',
255
- Values: [vpcId]
256
- },
257
- {
258
- Name: 'group-name',
259
- Values: ['default']
260
- }
261
- ]
262
- });
263
-
264
- const defaultResponse = await this.ec2Client.send(defaultSgCommand);
265
- if (defaultResponse.SecurityGroups && defaultResponse.SecurityGroups.length > 0) {
266
- return defaultResponse.SecurityGroups[0];
183
+ const defaultGroup = await this._findSecurityGroupByName(vpcId, 'default');
184
+ if (defaultGroup) {
185
+ return defaultGroup;
267
186
  }
268
-
187
+
269
188
  throw new Error(`No security group found for VPC ${vpcId}`);
270
189
  } catch (error) {
271
190
  console.error('Error finding default security group:', error);
@@ -273,45 +192,32 @@ class AWSDiscovery {
273
192
  }
274
193
  }
275
194
 
276
- /**
277
- * Find public subnets for NAT Gateway placement
278
- * @param {string} vpcId - The VPC ID to search within
279
- * @returns {Promise<Object>} First public subnet object for NAT Gateway placement
280
- * @throws {Error} If no public subnets are found in the VPC
281
- */
282
195
  async findPublicSubnets(vpcId) {
283
196
  try {
284
- const command = new DescribeSubnetsCommand({
285
- Filters: [
286
- {
287
- Name: 'vpc-id',
288
- Values: [vpcId]
289
- }
290
- ]
291
- });
292
-
293
- const response = await this.ec2Client.send(command);
294
-
295
- if (!response.Subnets || response.Subnets.length === 0) {
197
+ const subnets = await this._fetchSubnets(vpcId);
198
+
199
+ if (subnets.length === 0) {
296
200
  throw new Error(`No subnets found in VPC ${vpcId}`);
297
201
  }
298
202
 
299
- // Find public subnets (have direct route to IGW)
300
- const publicSubnets = [];
301
-
302
- for (const subnet of response.Subnets) {
303
- // Check route tables to determine if subnet is public
304
- const isPrivate = await this.isSubnetPrivate(subnet.SubnetId);
305
- if (!isPrivate) {
306
- publicSubnets.push(subnet);
307
- }
308
- }
203
+ const { publicSubnets } = await this._classifySubnets(subnets);
309
204
 
310
205
  if (publicSubnets.length === 0) {
311
- throw new Error(`No public subnets found in VPC ${vpcId} for NAT Gateway placement`);
206
+ console.warn(
207
+ `WARNING: No public subnets found in VPC ${vpcId}`
208
+ );
209
+ console.warn(
210
+ 'A public subnet with Internet Gateway route is required for NAT Gateway placement'
211
+ );
212
+ console.warn(
213
+ 'Please create a public subnet or use VPC endpoints instead'
214
+ );
215
+ return null;
312
216
  }
313
217
 
314
- // Return first public subnet for NAT Gateway
218
+ console.log(
219
+ `Found ${publicSubnets.length} public subnets, using ${publicSubnets[0].SubnetId} for NAT Gateway`
220
+ );
315
221
  return publicSubnets[0];
316
222
  } catch (error) {
317
223
  console.error('Error finding public subnets:', error);
@@ -319,91 +225,121 @@ class AWSDiscovery {
319
225
  }
320
226
  }
321
227
 
322
- /**
323
- * Find private route table for VPC endpoints
324
- * @param {string} vpcId - The VPC ID to search within
325
- * @returns {Promise<Object>} Route table object containing RouteTableId and other properties
326
- * @throws {Error} If no route tables are found for the VPC
327
- */
328
228
  async findPrivateRouteTable(vpcId) {
329
229
  try {
330
- const command = new DescribeRouteTablesCommand({
331
- Filters: [
332
- {
333
- Name: 'vpc-id',
334
- Values: [vpcId]
335
- }
336
- ]
337
- });
338
-
339
- const response = await this.ec2Client.send(command);
340
-
341
- if (!response.RouteTables || response.RouteTables.length === 0) {
230
+ const routeTables = await this.findRouteTables(vpcId);
231
+
232
+ if (routeTables.length === 0) {
342
233
  throw new Error(`No route tables found for VPC ${vpcId}`);
343
234
  }
344
235
 
345
- // Find a route table that doesn't have direct IGW route (private)
346
- for (const routeTable of response.RouteTables) {
347
- let hasIgwRoute = false;
348
- for (const route of routeTable.Routes || []) {
349
- if (route.GatewayId && route.GatewayId.startsWith('igw-')) {
350
- hasIgwRoute = true;
351
- break;
352
- }
353
- }
354
- if (!hasIgwRoute) {
355
- return routeTable;
356
- }
357
- }
236
+ const privateTable = routeTables.find(
237
+ (rt) => !this._findIgwRoute(rt)
238
+ );
358
239
 
359
- // If no private route table found, return the first one
360
- return response.RouteTables[0];
240
+ return privateTable || routeTables[0];
361
241
  } catch (error) {
362
242
  console.error('Error finding private route table:', error);
363
243
  throw error;
364
244
  }
365
245
  }
366
246
 
367
- /**
368
- * Find existing NAT Gateways in the VPC
369
- * @param {string} vpcId - The VPC ID to search within
370
- * @returns {Promise<Object|null>} NAT Gateway object or null if none found
371
- */
372
247
  async findExistingNatGateway(vpcId) {
373
248
  try {
374
249
  const command = new DescribeNatGatewaysCommand({
375
250
  Filter: [
376
251
  {
377
252
  Name: 'vpc-id',
378
- Values: [vpcId]
253
+ Values: [vpcId],
379
254
  },
380
255
  {
381
256
  Name: 'state',
382
- Values: ['available']
383
- }
384
- ]
257
+ Values: ['available'],
258
+ },
259
+ ],
385
260
  });
386
-
261
+
387
262
  const response = await this.ec2Client.send(command);
388
-
389
- if (response.NatGateways && response.NatGateways.length > 0) {
390
- // Find a NAT Gateway tagged for Frigg first
391
- const friggNatGateway = response.NatGateways.find(nat =>
392
- nat.Tags && nat.Tags.some(tag =>
393
- tag.Key === 'Name' && tag.Value.includes('frigg')
394
- )
263
+
264
+ const natGateways = (response.NatGateways || []).filter((nat) => {
265
+ if (nat.State !== 'available') {
266
+ console.warn(
267
+ `Skipping NAT Gateway ${nat.NatGatewayId} with state: ${nat.State}`
268
+ );
269
+ return false;
270
+ }
271
+ return true;
272
+ });
273
+
274
+ if (natGateways.length === 0) {
275
+ console.warn('No truly available NAT Gateways found in VPC');
276
+ return null;
277
+ }
278
+
279
+ const sortedNatGateways = natGateways.sort((a, b) => {
280
+ const aIsFrigg = this._isFriggManaged(a.Tags);
281
+ const bIsFrigg = this._isFriggManaged(b.Tags);
282
+
283
+ if (aIsFrigg && !bIsFrigg) return -1;
284
+ if (!aIsFrigg && bIsFrigg) return 1;
285
+ return 0;
286
+ });
287
+
288
+ for (const natGateway of sortedNatGateways) {
289
+ const subnetId = natGateway.SubnetId;
290
+ const isPrivate = await this.isSubnetPrivate(
291
+ subnetId,
292
+ natGateway.VpcId
395
293
  );
396
-
397
- if (friggNatGateway) {
398
- console.log(`Found existing Frigg NAT Gateway: ${friggNatGateway.NatGatewayId}`);
399
- return friggNatGateway;
294
+ const isFriggNat = this._isFriggManaged(natGateway.Tags);
295
+
296
+ if (isPrivate) {
297
+ console.warn(
298
+ `WARNING: NAT Gateway ${natGateway.NatGatewayId} is in subnet ${subnetId} which appears to be private`
299
+ );
300
+
301
+ if (isFriggNat) {
302
+ console.warn(
303
+ 'This is a Frigg-managed NAT Gateway that may have been misconfigured by route table changes'
304
+ );
305
+ console.warn(
306
+ 'Consider enabling selfHeal: true to fix this automatically'
307
+ );
308
+ natGateway._isInPrivateSubnet = true;
309
+ return natGateway;
310
+ }
311
+
312
+ console.warn(
313
+ 'NAT Gateways MUST be placed in public subnets with Internet Gateway routes'
314
+ );
315
+ console.warn('Skipping this misconfigured NAT Gateway...');
316
+ continue;
400
317
  }
401
-
402
- // Return first available NAT Gateway if no Frigg-specific one found
403
- console.log(`Found existing NAT Gateway: ${response.NatGateways[0].NatGatewayId}`);
404
- return response.NatGateways[0];
318
+
319
+ if (isFriggNat) {
320
+ console.log(
321
+ `Found existing Frigg-managed NAT Gateway: ${natGateway.NatGatewayId} (State: ${natGateway.State})`
322
+ );
323
+ natGateway._isInPrivateSubnet = false;
324
+ return natGateway;
325
+ }
326
+
327
+ console.log(
328
+ `Found existing NAT Gateway in public subnet: ${natGateway.NatGatewayId} (State: ${natGateway.State})`
329
+ );
330
+ natGateway._isInPrivateSubnet = false;
331
+ return natGateway;
405
332
  }
406
-
333
+
334
+ console.error(
335
+ `ERROR: Found ${(response.NatGateways || []).length} NAT Gateway(s) but all non-Frigg ones are in private subnets!`
336
+ );
337
+ console.error(
338
+ 'These NAT Gateways will not provide internet connectivity without route table fixes'
339
+ );
340
+ console.error(
341
+ 'Enable selfHeal: true to fix automatically or create a new NAT Gateway'
342
+ );
407
343
  return null;
408
344
  } catch (error) {
409
345
  console.warn('Error finding existing NAT Gateway:', error.message);
@@ -411,39 +347,38 @@ class AWSDiscovery {
411
347
  }
412
348
  }
413
349
 
414
- /**
415
- * Find available Elastic IPs
416
- * @returns {Promise<Object|null>} Available EIP object or null if none found
417
- */
418
350
  async findAvailableElasticIP() {
419
351
  try {
420
352
  const command = new DescribeAddressesCommand({});
421
353
  const response = await this.ec2Client.send(command);
422
-
354
+
423
355
  if (response.Addresses && response.Addresses.length > 0) {
424
- // Find an unassociated EIP first
425
- const availableEIP = response.Addresses.find(eip =>
426
- !eip.AssociationId && !eip.InstanceId && !eip.NetworkInterfaceId
356
+ const availableEIP = response.Addresses.find(
357
+ (eip) =>
358
+ !eip.AssociationId &&
359
+ !eip.InstanceId &&
360
+ !eip.NetworkInterfaceId
427
361
  );
428
-
362
+
429
363
  if (availableEIP) {
430
- console.log(`Found available Elastic IP: ${availableEIP.AllocationId}`);
364
+ console.log(
365
+ `Found available Elastic IP: ${availableEIP.AllocationId}`
366
+ );
431
367
  return availableEIP;
432
368
  }
433
-
434
- // Check for EIPs tagged for Frigg
435
- const friggEIP = response.Addresses.find(eip =>
436
- eip.Tags && eip.Tags.some(tag =>
437
- tag.Key === 'Name' && tag.Value.includes('frigg')
438
- )
369
+
370
+ const friggEIP = response.Addresses.find((eip) =>
371
+ this._isFriggManaged(eip.Tags)
439
372
  );
440
-
373
+
441
374
  if (friggEIP) {
442
- console.log(`Found Frigg-tagged Elastic IP: ${friggEIP.AllocationId}`);
375
+ console.log(
376
+ `Found Frigg-tagged Elastic IP: ${friggEIP.AllocationId}`
377
+ );
443
378
  return friggEIP;
444
379
  }
445
380
  }
446
-
381
+
447
382
  return null;
448
383
  } catch (error) {
449
384
  console.warn('Error finding available Elastic IP:', error.message);
@@ -451,120 +386,791 @@ class AWSDiscovery {
451
386
  }
452
387
  }
453
388
 
454
- /**
455
- * Find the default KMS key for the account
456
- * @returns {Promise<string|null>} KMS key ARN or null if no key found
457
- */
458
389
  async findDefaultKmsKey() {
390
+ console.log('KMS Discovery Starting...');
459
391
  try {
392
+ console.log(`[KMS Discovery] Running in region: ${this.region}`);
393
+ try {
394
+ const accountId = await this.getAccountId();
395
+ console.log(`[KMS Discovery] AWS Account ID: ${accountId}`);
396
+ } catch (error) {
397
+ console.warn(
398
+ '[KMS Discovery] Could not retrieve account ID:',
399
+ error.message
400
+ );
401
+ }
402
+
460
403
  const command = new ListKeysCommand({});
461
404
  const response = await this.kmsClient.send(command);
462
-
405
+
463
406
  if (!response.Keys || response.Keys.length === 0) {
464
- console.log('No KMS keys found in account');
407
+ console.log('[KMS Discovery] No KMS keys found in account');
465
408
  return null;
466
409
  }
467
410
 
468
- // Look for customer managed keys first
411
+ console.log(
412
+ `[KMS Discovery] Found ${response.Keys.length} total keys in account`
413
+ );
414
+ let keysExamined = 0;
415
+ let customerManagedKeys = 0;
416
+ let enabledKeys = 0;
417
+ let pendingDeletionKeys = 0;
418
+
469
419
  for (const key of response.Keys) {
470
420
  try {
471
- const describeCommand = new DescribeKeyCommand({ KeyId: key.KeyId });
472
- const keyDetails = await this.kmsClient.send(describeCommand);
473
-
474
- if (keyDetails.KeyMetadata &&
475
- keyDetails.KeyMetadata.KeyManager === 'CUSTOMER' &&
476
- keyDetails.KeyMetadata.KeyState === 'Enabled') {
477
- console.log(`Found customer managed KMS key: ${keyDetails.KeyMetadata.Arn}`);
478
- return keyDetails.KeyMetadata.Arn;
421
+ const describeCommand = new DescribeKeyCommand({
422
+ KeyId: key.KeyId,
423
+ });
424
+ const keyDetails = await this.kmsClient.send(
425
+ describeCommand
426
+ );
427
+ keysExamined++;
428
+
429
+ const metadata = keyDetails.KeyMetadata;
430
+ if (!metadata) {
431
+ continue;
432
+ }
433
+
434
+ console.log(`[KMS Discovery] Key ${key.KeyId}:`, {
435
+ KeyManager: metadata.KeyManager,
436
+ KeyState: metadata.KeyState,
437
+ Enabled: metadata.Enabled,
438
+ DeletionDate:
439
+ metadata.DeletionDate || 'Not scheduled for deletion',
440
+ Arn: metadata.Arn,
441
+ });
442
+
443
+ if (metadata.KeyManager === 'CUSTOMER') {
444
+ customerManagedKeys++;
445
+
446
+ if (metadata.KeyState === 'Enabled') {
447
+ enabledKeys++;
448
+ } else if (metadata.KeyState === 'PendingDeletion') {
449
+ pendingDeletionKeys++;
450
+ console.warn(
451
+ `[KMS Discovery] Skipping key ${key.KeyId} - State: PendingDeletion, DeletionDate: ${metadata.DeletionDate}`
452
+ );
453
+ }
454
+
455
+ if (
456
+ metadata.KeyState === 'Enabled' &&
457
+ !metadata.DeletionDate
458
+ ) {
459
+ console.log(
460
+ `[KMS Discovery] Found eligible customer managed KMS key: ${metadata.Arn}`
461
+ );
462
+ return metadata.Arn;
463
+ } else if (
464
+ metadata.KeyState === 'Enabled' &&
465
+ metadata.DeletionDate
466
+ ) {
467
+ console.error(
468
+ `[KMS Discovery] WARNING: Key ${key.KeyId} has KeyState='Enabled' but DeletionDate is set: ${metadata.DeletionDate}`
469
+ );
470
+ }
479
471
  }
480
472
  } catch (error) {
481
- // Continue to next key if we can't describe this one
482
- console.warn(`Could not describe key ${key.KeyId}:`, error.message);
473
+ console.warn(
474
+ `[KMS Discovery] Could not describe key ${key.KeyId}:`,
475
+ error.message
476
+ );
483
477
  continue;
484
478
  }
485
479
  }
486
480
 
487
- console.log('No customer managed KMS keys found');
481
+ console.log('[KMS Discovery] Summary:', {
482
+ totalKeys: response.Keys.length,
483
+ keysExamined,
484
+ customerManagedKeys,
485
+ enabledKeys,
486
+ pendingDeletionKeys,
487
+ });
488
+
489
+ if (customerManagedKeys === 0) {
490
+ console.log(
491
+ '[KMS Discovery] No customer managed KMS keys found in account'
492
+ );
493
+ } else if (enabledKeys === 0) {
494
+ console.warn(
495
+ '[KMS Discovery] Found customer managed keys but none are in Enabled state'
496
+ );
497
+ } else {
498
+ console.warn(
499
+ '[KMS Discovery] Found enabled customer managed keys but none met all criteria'
500
+ );
501
+ }
502
+
488
503
  return null;
489
504
  } catch (error) {
490
- console.error('Error finding default KMS key:', error);
505
+ console.error(
506
+ '[KMS Discovery] Error finding default KMS key:',
507
+ error
508
+ );
491
509
  return null;
492
510
  }
493
511
  }
494
512
 
495
- /**
496
- * Discover all AWS resources needed for Frigg deployment
497
- * @returns {Promise<Object>} Object containing discovered resource IDs:
498
- * @returns {string} return.defaultVpcId - The default VPC ID
499
- * @returns {string} return.defaultSecurityGroupId - The default security group ID
500
- * @returns {string} return.privateSubnetId1 - First private subnet ID
501
- * @returns {string} return.privateSubnetId2 - Second private subnet ID
502
- * @returns {string} return.publicSubnetId - Public subnet ID for NAT Gateway
503
- * @returns {string} return.privateRouteTableId - Private route table ID
504
- * @returns {string|null} return.defaultKmsKeyId - Default KMS key ARN or null if not found
505
- * @throws {Error} If resource discovery fails
506
- */
507
- async discoverResources() {
513
+ async detectMisconfiguredResources(vpcId) {
514
+ try {
515
+ const misconfigurations = {
516
+ natGatewaysInPrivateSubnets: [],
517
+ orphanedElasticIps: [],
518
+ misconfiguredRouteTables: [],
519
+ privateSubnetsWithoutNatRoute: [],
520
+ };
521
+
522
+ const natCommand = new DescribeNatGatewaysCommand({
523
+ Filter: [
524
+ { Name: 'vpc-id', Values: [vpcId] },
525
+ { Name: 'state', Values: ['available'] },
526
+ ],
527
+ });
528
+ const natResponse = await this.ec2Client.send(natCommand);
529
+
530
+ for (const nat of natResponse.NatGateways || []) {
531
+ const isPrivate = await this.isSubnetPrivate(nat.SubnetId, vpcId);
532
+ if (isPrivate) {
533
+ misconfigurations.natGatewaysInPrivateSubnets.push({
534
+ natGatewayId: nat.NatGatewayId,
535
+ subnetId: nat.SubnetId,
536
+ tags: nat.Tags,
537
+ });
538
+ }
539
+ }
540
+
541
+ const eipCommand = new DescribeAddressesCommand({});
542
+ const eipResponse = await this.ec2Client.send(eipCommand);
543
+
544
+ for (const eip of eipResponse.Addresses || []) {
545
+ if (
546
+ !eip.InstanceId &&
547
+ !eip.NetworkInterfaceId &&
548
+ !eip.AssociationId &&
549
+ this._isFriggManaged(eip.Tags)
550
+ ) {
551
+ misconfigurations.orphanedElasticIps.push({
552
+ allocationId: eip.AllocationId,
553
+ publicIp: eip.PublicIp,
554
+ tags: eip.Tags,
555
+ });
556
+ }
557
+ }
558
+
559
+ const subnets = await this.findPrivateSubnets(vpcId);
560
+ const routeTables = await this.findRouteTables(vpcId);
561
+
562
+ for (const subnet of subnets) {
563
+ const hasNatRoute = routeTables.some((rt) => {
564
+ const isAssociated = (rt.Associations || []).some(
565
+ (assoc) => assoc.SubnetId === subnet.SubnetId
566
+ );
567
+ if (!isAssociated) {
568
+ return false;
569
+ }
570
+ return (rt.Routes || []).some(
571
+ (route) =>
572
+ route.NatGatewayId &&
573
+ route.DestinationCidrBlock === '0.0.0.0/0'
574
+ );
575
+ });
576
+
577
+ if (!hasNatRoute) {
578
+ misconfigurations.privateSubnetsWithoutNatRoute.push({
579
+ subnetId: subnet.SubnetId,
580
+ availabilityZone: subnet.AvailabilityZone,
581
+ });
582
+ }
583
+ }
584
+
585
+ return misconfigurations;
586
+ } catch (error) {
587
+ console.error('Error detecting misconfigurations:', error);
588
+ return {
589
+ natGatewaysInPrivateSubnets: [],
590
+ orphanedElasticIps: [],
591
+ misconfiguredRouteTables: [],
592
+ privateSubnetsWithoutNatRoute: [],
593
+ };
594
+ }
595
+ }
596
+
597
+ getHealingRecommendations(misconfigurations) {
598
+ const recommendations = [];
599
+
600
+ if (misconfigurations.natGatewaysInPrivateSubnets.length > 0) {
601
+ recommendations.push({
602
+ severity: 'critical',
603
+ issue: 'NAT Gateway in private subnet',
604
+ recommendation:
605
+ 'Recreate NAT Gateway in public subnet or fix route tables',
606
+ affectedResources:
607
+ misconfigurations.natGatewaysInPrivateSubnets.map(
608
+ (n) => n.natGatewayId
609
+ ),
610
+ });
611
+ }
612
+
613
+ if (misconfigurations.orphanedElasticIps.length > 0) {
614
+ recommendations.push({
615
+ severity: 'warning',
616
+ issue: 'Orphaned Elastic IPs',
617
+ recommendation: 'Release unused Elastic IPs to avoid charges',
618
+ affectedResources: misconfigurations.orphanedElasticIps.map(
619
+ (e) => e.allocationId
620
+ ),
621
+ });
622
+ }
623
+
624
+ if (misconfigurations.privateSubnetsWithoutNatRoute.length > 0) {
625
+ recommendations.push({
626
+ severity: 'critical',
627
+ issue: 'Private subnets without NAT route',
628
+ recommendation:
629
+ 'Add NAT Gateway route to private subnet route tables',
630
+ affectedResources:
631
+ misconfigurations.privateSubnetsWithoutNatRoute.map(
632
+ (s) => s.subnetId
633
+ ),
634
+ });
635
+ }
636
+
637
+ recommendations.sort((a, b) => {
638
+ const severityOrder = { critical: 0, warning: 1, info: 2 };
639
+ return severityOrder[a.severity] - severityOrder[b.severity];
640
+ });
641
+
642
+ return recommendations;
643
+ }
644
+
645
+ async discoverResources(options = {}) {
508
646
  try {
509
- console.log('Discovering AWS resources for Frigg deployment...');
510
-
647
+ console.log(
648
+ '\nšŸš€ Discovering AWS resources for Frigg deployment...'
649
+ );
650
+ console.log('═'.repeat(60));
651
+
511
652
  const vpc = await this.findDefaultVpc();
512
- console.log(`Found VPC: ${vpc.VpcId}`);
513
-
514
- const privateSubnets = await this.findPrivateSubnets(vpc.VpcId);
515
- console.log(`Found ${privateSubnets.length} private subnets: ${privateSubnets.map(s => s.SubnetId).join(', ')}`);
516
-
653
+ console.log(`\nāœ… Found VPC: ${vpc.VpcId}`);
654
+
655
+ const autoConvert = options.selfHeal || false;
656
+
657
+ const privateSubnets = await this.findPrivateSubnets(
658
+ vpc.VpcId,
659
+ autoConvert
660
+ );
661
+ console.log(
662
+ `\nāœ… Selected subnets for Lambda: ${privateSubnets
663
+ .map((s) => s.SubnetId)
664
+ .join(', ')}`
665
+ );
666
+
517
667
  const publicSubnet = await this.findPublicSubnets(vpc.VpcId);
518
- console.log(`Found public subnet for NAT Gateway: ${publicSubnet.SubnetId}`);
519
-
520
- const securityGroup = await this.findDefaultSecurityGroup(vpc.VpcId);
521
- console.log(`Found security group: ${securityGroup.GroupId}`);
522
-
668
+ if (publicSubnet) {
669
+ console.log(
670
+ `\nāœ… Found public subnet for NAT Gateway: ${publicSubnet.SubnetId}`
671
+ );
672
+ } else {
673
+ console.log(
674
+ `\nāš ļø No public subnet found - NAT Gateway creation may fail`
675
+ );
676
+ }
677
+
678
+ const securityGroup = await this.findDefaultSecurityGroup(
679
+ vpc.VpcId
680
+ );
681
+ console.log(`\nāœ… Found security group: ${securityGroup.GroupId}`);
682
+
523
683
  const routeTable = await this.findPrivateRouteTable(vpc.VpcId);
524
- console.log(`Found route table: ${routeTable.RouteTableId}`);
525
-
684
+ console.log(`āœ… Found route table: ${routeTable.RouteTableId}`);
685
+
526
686
  const kmsKeyArn = await this.findDefaultKmsKey();
527
687
  if (kmsKeyArn) {
528
- console.log(`Found KMS key: ${kmsKeyArn}`);
688
+ console.log(`āœ… Found KMS key: ${kmsKeyArn}`);
529
689
  } else {
530
- console.log('No KMS key found');
690
+ console.log('ā„¹ļø No KMS key found');
531
691
  }
532
-
533
- // Try to find existing NAT Gateway
534
- const existingNatGateway = await this.findExistingNatGateway(vpc.VpcId);
692
+
693
+ const existingNatGateway = await this.findExistingNatGateway(
694
+ vpc.VpcId
695
+ );
535
696
  let natGatewayId = null;
536
697
  let elasticIpAllocationId = null;
537
-
698
+ let natGatewayInPrivateSubnet = false;
699
+
538
700
  if (existingNatGateway) {
539
701
  natGatewayId = existingNatGateway.NatGatewayId;
540
- // Get the EIP allocation ID from the NAT Gateway
541
- if (existingNatGateway.NatGatewayAddresses && existingNatGateway.NatGatewayAddresses.length > 0) {
542
- elasticIpAllocationId = existingNatGateway.NatGatewayAddresses[0].AllocationId;
702
+ natGatewayInPrivateSubnet =
703
+ existingNatGateway._isInPrivateSubnet || false;
704
+
705
+ if (
706
+ existingNatGateway.NatGatewayAddresses &&
707
+ existingNatGateway.NatGatewayAddresses.length > 0
708
+ ) {
709
+ elasticIpAllocationId =
710
+ existingNatGateway.NatGatewayAddresses[0].AllocationId;
543
711
  }
544
712
  } else {
545
- // If no NAT Gateway exists, check for available EIP
546
713
  const availableEIP = await this.findAvailableElasticIP();
547
714
  if (availableEIP) {
548
715
  elasticIpAllocationId = availableEIP.AllocationId;
549
716
  }
550
717
  }
551
718
 
719
+ const subnet1IsActuallyPrivate = privateSubnets[0]
720
+ ? await this.isSubnetPrivate(
721
+ privateSubnets[0].SubnetId,
722
+ privateSubnets[0].VpcId || vpc.VpcId
723
+ )
724
+ : false;
725
+ const subnet2IsActuallyPrivate = privateSubnets[1]
726
+ ? await this.isSubnetPrivate(
727
+ privateSubnets[1].SubnetId,
728
+ privateSubnets[1].VpcId || vpc.VpcId
729
+ )
730
+ : subnet1IsActuallyPrivate;
731
+
732
+ const subnetStatus = {
733
+ requiresConversion:
734
+ !subnet1IsActuallyPrivate || !subnet2IsActuallyPrivate,
735
+ subnet1NeedsConversion: !subnet1IsActuallyPrivate,
736
+ subnet2NeedsConversion: !subnet2IsActuallyPrivate,
737
+ };
738
+
739
+ if (subnetStatus.requiresConversion) {
740
+ console.log(`\nāš ļø SUBNET CONFIGURATION WARNING:`);
741
+ if (subnetStatus.subnet1NeedsConversion && privateSubnets[0]) {
742
+ console.log(
743
+ ` - Subnet ${privateSubnets[0].SubnetId} is currently PUBLIC but will be used for Lambda`
744
+ );
745
+ }
746
+ if (subnetStatus.subnet2NeedsConversion && privateSubnets[1]) {
747
+ console.log(
748
+ ` - Subnet ${privateSubnets[1].SubnetId} is currently PUBLIC but will be used for Lambda`
749
+ );
750
+ }
751
+ console.log(
752
+ ` šŸ’” Enable selfHeal: true to automatically fix this`
753
+ );
754
+ }
755
+
756
+ console.log(`\n${'═'.repeat(60)}`);
757
+ console.log('šŸ“‹ Discovery Summary:');
758
+ console.log(` VPC: ${vpc.VpcId}`);
759
+ console.log(
760
+ ` Lambda Subnets: ${privateSubnets
761
+ .map((s) => s.SubnetId)
762
+ .join(', ')}`
763
+ );
764
+ console.log(
765
+ ` NAT Subnet: ${
766
+ publicSubnet?.SubnetId || 'None (needs creation)'
767
+ }`
768
+ );
769
+ console.log(
770
+ ` NAT Gateway: ${natGatewayId || 'None (will be created)'}`
771
+ );
772
+ console.log(
773
+ ` Elastic IP: ${
774
+ elasticIpAllocationId || 'None (will be allocated)'
775
+ }`
776
+ );
777
+ if (subnetStatus.requiresConversion) {
778
+ console.log(` āš ļø Subnet Conversion Required: Yes`);
779
+ }
780
+ console.log(`${'═'.repeat(60)}\n`);
781
+
552
782
  return {
553
783
  defaultVpcId: vpc.VpcId,
784
+ vpcCidr: vpc.CidrBlock,
554
785
  defaultSecurityGroupId: securityGroup.GroupId,
555
786
  privateSubnetId1: privateSubnets[0]?.SubnetId,
556
- privateSubnetId2: privateSubnets[1]?.SubnetId || privateSubnets[0]?.SubnetId,
557
- publicSubnetId: publicSubnet.SubnetId,
787
+ privateSubnetId2:
788
+ privateSubnets[1]?.SubnetId || privateSubnets[0]?.SubnetId,
789
+ publicSubnetId: publicSubnet?.SubnetId || null,
558
790
  privateRouteTableId: routeTable.RouteTableId,
559
791
  defaultKmsKeyId: kmsKeyArn,
560
792
  existingNatGatewayId: natGatewayId,
561
- existingElasticIpAllocationId: elasticIpAllocationId
793
+ existingElasticIpAllocationId: elasticIpAllocationId,
794
+ natGatewayInPrivateSubnet: natGatewayInPrivateSubnet,
795
+ subnetConversionRequired: subnetStatus.requiresConversion,
796
+ privateSubnetsWithWrongRoutes: (() => {
797
+ const wrongRoutes = [];
798
+ if (
799
+ subnetStatus.subnet1NeedsConversion &&
800
+ privateSubnets[0]
801
+ ) {
802
+ wrongRoutes.push(privateSubnets[0].SubnetId);
803
+ }
804
+ if (
805
+ subnetStatus.subnet2NeedsConversion &&
806
+ privateSubnets[1]
807
+ ) {
808
+ wrongRoutes.push(privateSubnets[1].SubnetId);
809
+ }
810
+ return wrongRoutes;
811
+ })(),
562
812
  };
563
813
  } catch (error) {
564
814
  console.error('Error discovering AWS resources:', error);
565
815
  throw error;
566
816
  }
567
817
  }
818
+
819
+ async findInternetGateway(vpcId) {
820
+ try {
821
+ const command = new DescribeInternetGatewaysCommand({
822
+ Filters: [
823
+ {
824
+ Name: 'attachment.vpc-id',
825
+ Values: [vpcId],
826
+ },
827
+ {
828
+ Name: 'attachment.state',
829
+ Values: ['available'],
830
+ },
831
+ ],
832
+ });
833
+
834
+ const response = await this.ec2Client.send(command);
835
+
836
+ if (
837
+ response.InternetGateways &&
838
+ response.InternetGateways.length > 0
839
+ ) {
840
+ console.log(
841
+ `Found existing Internet Gateway: ${response.InternetGateways[0].InternetGatewayId}`
842
+ );
843
+ return response.InternetGateways[0];
844
+ }
845
+
846
+ return null;
847
+ } catch (error) {
848
+ console.warn('Error finding Internet Gateway:', error.message);
849
+ return null;
850
+ }
851
+ }
852
+
853
+ async findFriggManagedResources(serviceName, stage) {
854
+ const results = {
855
+ natGateways: [],
856
+ elasticIps: [],
857
+ routeTables: [],
858
+ subnets: [],
859
+ securityGroups: [],
860
+ };
861
+
862
+ try {
863
+ const filters = [
864
+ {
865
+ Name: 'tag:ManagedBy',
866
+ Values: ['Frigg'],
867
+ },
868
+ ];
869
+
870
+ if (serviceName) {
871
+ filters.push({
872
+ Name: 'tag:Service',
873
+ Values: [serviceName],
874
+ });
875
+ }
876
+
877
+ if (stage) {
878
+ filters.push({
879
+ Name: 'tag:Stage',
880
+ Values: [stage],
881
+ });
882
+ }
883
+
884
+ const fetchWithFallback = async (Command, input, field, label) => {
885
+ try {
886
+ const response = await this.ec2Client.send(
887
+ new Command(input)
888
+ );
889
+ return response[field] || [];
890
+ } catch (err) {
891
+ console.warn(
892
+ `Error finding Frigg ${label}:`,
893
+ err.message
894
+ );
895
+ return [];
896
+ }
897
+ };
898
+
899
+ results.natGateways = await fetchWithFallback(
900
+ DescribeNatGatewaysCommand,
901
+ {
902
+ Filter: [
903
+ ...filters,
904
+ {
905
+ Name: 'state',
906
+ Values: ['available'],
907
+ },
908
+ ],
909
+ },
910
+ 'NatGateways',
911
+ 'NAT Gateways'
912
+ );
913
+
914
+ results.elasticIps = await fetchWithFallback(
915
+ DescribeAddressesCommand,
916
+ { Filters: filters },
917
+ 'Addresses',
918
+ 'Elastic IPs'
919
+ );
920
+
921
+ results.routeTables = await fetchWithFallback(
922
+ DescribeRouteTablesCommand,
923
+ { Filters: filters },
924
+ 'RouteTables',
925
+ 'Route Tables'
926
+ );
927
+
928
+ results.subnets = await fetchWithFallback(
929
+ DescribeSubnetsCommand,
930
+ { Filters: filters },
931
+ 'Subnets',
932
+ 'Subnets'
933
+ );
934
+
935
+ results.securityGroups = await fetchWithFallback(
936
+ DescribeSecurityGroupsCommand,
937
+ { Filters: filters },
938
+ 'SecurityGroups',
939
+ 'Security Groups'
940
+ );
941
+
942
+ console.log('Found Frigg-managed resources:', {
943
+ natGateways: results.natGateways.length,
944
+ elasticIps: results.elasticIps.length,
945
+ routeTables: results.routeTables.length,
946
+ subnets: results.subnets.length,
947
+ securityGroups: results.securityGroups.length,
948
+ });
949
+
950
+ return results;
951
+ } catch (error) {
952
+ console.error('Error finding Frigg-managed resources:', error);
953
+ return results;
954
+ }
955
+ }
956
+
957
+ async findRouteTables(vpcId) {
958
+ const command = new DescribeRouteTablesCommand({
959
+ Filters: [
960
+ {
961
+ Name: 'vpc-id',
962
+ Values: [vpcId],
963
+ },
964
+ ],
965
+ });
966
+ const response = await this.ec2Client.send(command);
967
+ return response.RouteTables || [];
968
+ }
969
+
970
+ async _fetchSubnets(vpcId) {
971
+ const command = new DescribeSubnetsCommand({
972
+ Filters: [
973
+ {
974
+ Name: 'vpc-id',
975
+ Values: [vpcId],
976
+ },
977
+ ],
978
+ });
979
+ const response = await this.ec2Client.send(command);
980
+ return response.Subnets || [];
981
+ }
982
+
983
+ async _getSubnetVpcId(subnetId) {
984
+ const command = new DescribeSubnetsCommand({
985
+ SubnetIds: [subnetId],
986
+ });
987
+ const response = await this.ec2Client.send(command);
988
+
989
+ if (!response.Subnets || response.Subnets.length === 0) {
990
+ throw new Error(`Subnet ${subnetId} not found`);
991
+ }
992
+
993
+ return response.Subnets[0].VpcId;
994
+ }
995
+
996
+ async _classifySubnets(subnets, { logDetails = false } = {}) {
997
+ const privateSubnets = [];
998
+ const publicSubnets = [];
999
+
1000
+ for (const subnet of subnets) {
1001
+ const isPrivate = await this.isSubnetPrivate(
1002
+ subnet.SubnetId,
1003
+ subnet.VpcId
1004
+ );
1005
+ if (isPrivate) {
1006
+ privateSubnets.push(subnet);
1007
+ if (logDetails) {
1008
+ console.log(
1009
+ ` šŸ”’ Private subnet: ${subnet.SubnetId} (AZ: ${subnet.AvailabilityZone})`
1010
+ );
1011
+ }
1012
+ } else {
1013
+ publicSubnets.push(subnet);
1014
+ if (logDetails) {
1015
+ console.log(
1016
+ ` 🌐 Public subnet: ${subnet.SubnetId} (AZ: ${subnet.AvailabilityZone})`
1017
+ );
1018
+ }
1019
+ }
1020
+ }
1021
+
1022
+ return { privateSubnets, publicSubnets };
1023
+ }
1024
+
1025
+ _logSubnetSummary(privateCount, publicCount) {
1026
+ console.log(`\nšŸ“Š Subnet Analysis Results:`);
1027
+ console.log(` - Private subnets: ${privateCount}`);
1028
+ console.log(` - Public subnets: ${publicCount}`);
1029
+ }
1030
+
1031
+ _selectSubnetsForLambda({ privateSubnets, publicSubnets, autoConvert, vpcId }) {
1032
+ if (privateSubnets.length >= 2) {
1033
+ console.log(
1034
+ `āœ… Found ${privateSubnets.length} private subnets for Lambda deployment`
1035
+ );
1036
+ return privateSubnets.slice(0, 2);
1037
+ }
1038
+
1039
+ if (privateSubnets.length === 1) {
1040
+ console.warn(
1041
+ `āš ļø Only 1 private subnet found. Need at least 2 for high availability.`
1042
+ );
1043
+ if (publicSubnets.length > 0 && autoConvert) {
1044
+ console.log(
1045
+ `šŸ”„ Will convert 1 public subnet to private for high availability...`
1046
+ );
1047
+ }
1048
+ return [...privateSubnets, ...publicSubnets].slice(0, 2);
1049
+ }
1050
+
1051
+ if (privateSubnets.length === 0 && publicSubnets.length > 0) {
1052
+ console.error(
1053
+ `āŒ CRITICAL: No private subnets found, but ${publicSubnets.length} public subnets exist`
1054
+ );
1055
+ console.error(
1056
+ `āŒ Lambda functions should NOT be deployed in public subnets!`
1057
+ );
1058
+
1059
+ if (autoConvert && publicSubnets.length >= 3) {
1060
+ console.log(
1061
+ `\nšŸ”§ AUTO-CONVERSION: Will configure subnets for proper isolation...`
1062
+ );
1063
+ console.log(
1064
+ ` - Keeping ${publicSubnets[0].SubnetId} as public (for NAT Gateway)`
1065
+ );
1066
+ console.log(
1067
+ ` - Converting ${publicSubnets[1].SubnetId} to private (for Lambda)`
1068
+ );
1069
+ if (publicSubnets[2]) {
1070
+ console.log(
1071
+ ` - Converting ${publicSubnets[2].SubnetId} to private (for Lambda)`
1072
+ );
1073
+ }
1074
+ return publicSubnets.slice(1, 3);
1075
+ }
1076
+
1077
+ if (autoConvert && publicSubnets.length >= 2) {
1078
+ console.log(
1079
+ `\nšŸ”§ AUTO-CONVERSION: Only ${publicSubnets.length} subnets available`
1080
+ );
1081
+ console.log(
1082
+ ` - Will need to create new subnets or reconfigure existing ones`
1083
+ );
1084
+ return publicSubnets.slice(0, 2);
1085
+ }
1086
+
1087
+ console.error(`\nāš ļø CONFIGURATION ERROR:`);
1088
+ console.error(
1089
+ ` Found ${publicSubnets.length} public subnets but no private subnets.`
1090
+ );
1091
+ console.error(
1092
+ ` Lambda functions require private subnets for security.`
1093
+ );
1094
+ console.error(`\n Options:`);
1095
+ console.error(
1096
+ ` 1. Enable selfHeal: true in vpc configuration`
1097
+ );
1098
+ console.error(` 2. Create private subnets manually`);
1099
+ console.error(
1100
+ ` 3. Set subnets.management: 'create' to create new private subnets`
1101
+ );
1102
+
1103
+ throw new Error(
1104
+ `No private subnets found in VPC ${vpcId}. ` +
1105
+ `Found ${publicSubnets.length} public subnets. ` +
1106
+ `Lambda requires private subnets. Enable selfHeal or create private subnets.`
1107
+ );
1108
+ }
1109
+
1110
+ return null;
1111
+ }
1112
+
1113
+ _findRouteTableForSubnet(routeTables, subnetId) {
1114
+ for (const rt of routeTables) {
1115
+ for (const assoc of rt.Associations || []) {
1116
+ if (assoc.SubnetId === subnetId) {
1117
+ return rt;
1118
+ }
1119
+ }
1120
+ }
1121
+
1122
+ for (const rt of routeTables) {
1123
+ for (const assoc of rt.Associations || []) {
1124
+ if (assoc.Main === true) {
1125
+ return rt;
1126
+ }
1127
+ }
1128
+ }
1129
+
1130
+ return null;
1131
+ }
1132
+
1133
+ _findIgwRoute(routeTable) {
1134
+ for (const route of routeTable.Routes || []) {
1135
+ if (route.GatewayId && route.GatewayId.startsWith('igw-')) {
1136
+ return route.GatewayId;
1137
+ }
1138
+ }
1139
+ return null;
1140
+ }
1141
+
1142
+ _isFriggManaged(tags) {
1143
+ if (!tags) {
1144
+ return false;
1145
+ }
1146
+
1147
+ return tags.some(
1148
+ (tag) =>
1149
+ (tag.Key === 'ManagedBy' && tag.Value === 'Frigg') ||
1150
+ (tag.Key === 'Name' &&
1151
+ typeof tag.Value === 'string' &&
1152
+ tag.Value.includes('frigg'))
1153
+ );
1154
+ }
1155
+
1156
+ async _findSecurityGroupByName(vpcId, groupName) {
1157
+ const command = new DescribeSecurityGroupsCommand({
1158
+ Filters: [
1159
+ {
1160
+ Name: 'vpc-id',
1161
+ Values: [vpcId],
1162
+ },
1163
+ {
1164
+ Name: 'group-name',
1165
+ Values: [groupName],
1166
+ },
1167
+ ],
1168
+ });
1169
+
1170
+ const response = await this.ec2Client.send(command);
1171
+ const groups = response.SecurityGroups || [];
1172
+ return groups[0] || null;
1173
+ }
568
1174
  }
569
1175
 
570
- module.exports = { AWSDiscovery };
1176
+ module.exports = { AWSDiscovery };