@friggframework/devtools 2.0.0--canary.428.d54dca5.0 → 2.0.0--canary.428.2abc64a.0

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.
@@ -43,15 +43,7 @@ function loadSTS() {
43
43
  }
44
44
  }
45
45
 
46
- /**
47
- * AWS Resource Discovery utilities for Frigg applications
48
- * These functions use AWS credentials to discover default resources during build time
49
- */
50
46
  class AWSDiscovery {
51
- /**
52
- * Creates an instance of AWSDiscovery
53
- * @param {string} [region='us-east-1'] - AWS region to use for discovery
54
- */
55
47
  constructor(region = 'us-east-1') {
56
48
  this.region = region;
57
49
  loadEC2();
@@ -62,11 +54,6 @@ class AWSDiscovery {
62
54
  this.stsClient = new STSClient({ region });
63
55
  }
64
56
 
65
- /**
66
- * Get AWS account ID
67
- * @returns {Promise<string>} The AWS account ID
68
- * @throws {Error} If unable to retrieve account ID
69
- */
70
57
  async getAccountId() {
71
58
  try {
72
59
  const command = new GetCallerIdentityCommand({});
@@ -78,11 +65,6 @@ class AWSDiscovery {
78
65
  }
79
66
  }
80
67
 
81
- /**
82
- * Find the default VPC for the account
83
- * @returns {Promise<Object>} VPC object containing VpcId and other properties
84
- * @throws {Error} If no VPC is found in the account
85
- */
86
68
  async findDefaultVpc() {
87
69
  try {
88
70
  const command = new DescribeVpcsCommand({
@@ -100,7 +82,6 @@ class AWSDiscovery {
100
82
  return response.Vpcs[0];
101
83
  }
102
84
 
103
- // If no default VPC, get the first available VPC
104
85
  const allVpcsCommand = new DescribeVpcsCommand({});
105
86
  const allVpcsResponse = await this.ec2Client.send(allVpcsCommand);
106
87
 
@@ -116,144 +97,34 @@ class AWSDiscovery {
116
97
  }
117
98
  }
118
99
 
119
- /**
120
- * Find private subnets for the given VPC
121
- * @param {string} vpcId - The VPC ID to search within
122
- * @param {boolean} autoConvert - If true, convert public subnets to private if needed
123
- * @returns {Promise<Array>} Array of subnet objects (at least 2 for high availability)
124
- * @throws {Error} If no subnets are found in the VPC
125
- */
126
100
  async findPrivateSubnets(vpcId, autoConvert = false) {
127
101
  try {
128
- const command = new DescribeSubnetsCommand({
129
- Filters: [
130
- {
131
- Name: 'vpc-id',
132
- Values: [vpcId],
133
- },
134
- ],
135
- });
102
+ const subnets = await this._fetchSubnets(vpcId);
136
103
 
137
- const response = await this.ec2Client.send(command);
138
-
139
- if (!response.Subnets || response.Subnets.length === 0) {
104
+ if (subnets.length === 0) {
140
105
  throw new Error(`No subnets found in VPC ${vpcId}`);
141
106
  }
142
107
 
143
- console.log(
144
- `\nšŸ” Analyzing ${response.Subnets.length} subnets in VPC ${vpcId}...`
145
- );
146
-
147
- // Categorize subnets by their actual routing
148
- const privateSubnets = [];
149
- const publicSubnets = [];
150
-
151
- for (const subnet of response.Subnets) {
152
- // Check route tables to determine if subnet is private
153
- const isPrivate = await this.isSubnetPrivate(subnet.SubnetId);
154
- if (isPrivate) {
155
- privateSubnets.push(subnet);
156
- console.log(
157
- ` šŸ”’ Private subnet: ${subnet.SubnetId} (AZ: ${subnet.AvailabilityZone})`
158
- );
159
- } else {
160
- publicSubnets.push(subnet);
161
- console.log(
162
- ` 🌐 Public subnet: ${subnet.SubnetId} (AZ: ${subnet.AvailabilityZone})`
163
- );
164
- }
165
- }
166
-
167
- console.log(`\nšŸ“Š Subnet Analysis Results:`);
168
- console.log(` - Private subnets: ${privateSubnets.length}`);
169
- console.log(` - Public subnets: ${publicSubnets.length}`);
170
-
171
- // If we have at least 2 private subnets, use them
172
- if (privateSubnets.length >= 2) {
173
- console.log(
174
- `āœ… Found ${privateSubnets.length} private subnets for Lambda deployment`
175
- );
176
- return privateSubnets.slice(0, 2);
177
- }
178
-
179
- // If we have 1 private subnet, we need at least one more
180
- if (privateSubnets.length === 1) {
181
- console.warn(
182
- `āš ļø Only 1 private subnet found. Need at least 2 for high availability.`
183
- );
184
- if (publicSubnets.length > 0 && autoConvert) {
185
- console.log(
186
- `šŸ”„ Will convert 1 public subnet to private for high availability...`
187
- );
188
- // Note: The actual conversion happens in the serverless template
189
- }
190
- // Return what we have - mix of private and public if needed
191
- return [...privateSubnets, ...publicSubnets].slice(0, 2);
192
- }
108
+ console.log(`\nšŸ” Analyzing ${subnets.length} subnets in VPC ${vpcId}...`);
193
109
 
194
- // No private subnets found at all - this is a problem!
195
- if (privateSubnets.length === 0 && publicSubnets.length > 0) {
196
- console.error(
197
- `āŒ CRITICAL: No private subnets found, but ${publicSubnets.length} public subnets exist`
198
- );
199
- console.error(
200
- `āŒ Lambda functions should NOT be deployed in public subnets!`
201
- );
110
+ const { privateSubnets, publicSubnets } = await this._classifySubnets(
111
+ subnets,
112
+ { logDetails: true }
113
+ );
202
114
 
203
- if (autoConvert && publicSubnets.length >= 3) {
204
- console.log(
205
- `\nšŸ”§ AUTO-CONVERSION: Will configure subnets for proper isolation...`
206
- );
207
- console.log(
208
- ` - Keeping ${publicSubnets[0].SubnetId} as public (for NAT Gateway)`
209
- );
210
- console.log(
211
- ` - Converting ${publicSubnets[1].SubnetId} to private (for Lambda)`
212
- );
213
- if (publicSubnets[2]) {
214
- console.log(
215
- ` - Converting ${publicSubnets[2].SubnetId} to private (for Lambda)`
216
- );
217
- }
115
+ this._logSubnetSummary(privateSubnets.length, publicSubnets.length);
218
116
 
219
- // Return subnets that SHOULD be private (indexes 1 and 2)
220
- // The actual conversion happens in the serverless template
221
- return publicSubnets.slice(1, 3);
222
- } else if (autoConvert && publicSubnets.length >= 2) {
223
- console.log(
224
- `\nšŸ”§ AUTO-CONVERSION: Only ${publicSubnets.length} subnets available`
225
- );
226
- console.log(
227
- ` - Will need to create new subnets or reconfigure existing ones`
228
- );
229
- // Return what we have but flag for conversion
230
- return publicSubnets.slice(0, 2);
231
- } else {
232
- console.error(`\nāš ļø CONFIGURATION ERROR:`);
233
- console.error(
234
- ` Found ${publicSubnets.length} public subnets but no private subnets.`
235
- );
236
- console.error(
237
- ` Lambda functions require private subnets for security.`
238
- );
239
- console.error(`\n Options:`);
240
- console.error(
241
- ` 1. Enable selfHeal: true in vpc configuration`
242
- );
243
- console.error(` 2. Create private subnets manually`);
244
- console.error(
245
- ` 3. Set subnets.management: 'create' to create new private subnets`
246
- );
117
+ const selection = this._selectSubnetsForLambda({
118
+ privateSubnets,
119
+ publicSubnets,
120
+ autoConvert,
121
+ vpcId,
122
+ });
247
123
 
248
- throw new Error(
249
- `No private subnets found in VPC ${vpcId}. ` +
250
- `Found ${publicSubnets.length} public subnets. ` +
251
- `Lambda requires private subnets. Enable selfHeal or create private subnets.`
252
- );
253
- }
124
+ if (selection) {
125
+ return selection;
254
126
  }
255
127
 
256
- // No subnets at all?
257
128
  throw new Error(`No subnets found in VPC ${vpcId}`);
258
129
  } catch (error) {
259
130
  console.error('Error finding private subnets:', error);
@@ -261,168 +132,57 @@ class AWSDiscovery {
261
132
  }
262
133
  }
263
134
 
264
- /**
265
- * Check if a subnet is private (no direct route to Internet Gateway)
266
- * @param {string} subnetId - The subnet ID to check
267
- * @returns {Promise<boolean>} True if subnet is private, false if public
268
- */
269
- /**
270
- * Validate if a subnet is truly public (has IGW route)
271
- * @param {string} subnetId - The subnet ID to validate
272
- * @returns {Promise<boolean>} true if public (has IGW route), false if private
273
- */
274
- async isSubnetPublic(subnetId) {
275
- const isPrivate = await this.isSubnetPrivate(subnetId);
135
+ async isSubnetPublic(subnetId, vpcId) {
136
+ const isPrivate = await this.isSubnetPrivate(subnetId, vpcId);
276
137
  return !isPrivate;
277
138
  }
278
139
 
279
- async isSubnetPrivate(subnetId) {
140
+ async isSubnetPrivate(subnetId, vpcId) {
280
141
  try {
281
- // First, get the subnet details to find its VPC
282
- const subnetCommand = new DescribeSubnetsCommand({
283
- SubnetIds: [subnetId],
284
- });
285
- const subnetResponse = await this.ec2Client.send(subnetCommand);
286
-
287
- if (
288
- !subnetResponse.Subnets ||
289
- subnetResponse.Subnets.length === 0
290
- ) {
291
- throw new Error(`Subnet ${subnetId} not found`);
292
- }
293
-
294
- const subnet = subnetResponse.Subnets[0];
295
- const vpcId = subnet.VpcId;
296
-
297
- // Get all route tables for this VPC
298
- const routeTablesCommand = new DescribeRouteTablesCommand({
299
- Filters: [
300
- {
301
- Name: 'vpc-id',
302
- Values: [vpcId],
303
- },
304
- ],
305
- });
306
-
307
- const routeTablesResponse = await this.ec2Client.send(
308
- routeTablesCommand
309
- );
310
-
311
- // Find the route table for this subnet
312
- let routeTable = null;
313
-
314
- // First check for explicit association
315
- for (const rt of routeTablesResponse.RouteTables || []) {
316
- for (const assoc of rt.Associations || []) {
317
- if (assoc.SubnetId === subnetId) {
318
- routeTable = rt;
319
- break;
320
- }
321
- }
322
- if (routeTable) break;
323
- }
142
+ const targetVpcId = vpcId || (await this._getSubnetVpcId(subnetId));
324
143
 
325
- // If no explicit association, use the main route table
326
- if (!routeTable) {
327
- for (const rt of routeTablesResponse.RouteTables || []) {
328
- for (const assoc of rt.Associations || []) {
329
- if (assoc.Main === true) {
330
- routeTable = rt;
331
- break;
332
- }
333
- }
334
- if (routeTable) break;
335
- }
336
- }
144
+ const routeTables = await this.findRouteTables(targetVpcId);
145
+ const routeTable = this._findRouteTableForSubnet(routeTables, subnetId);
337
146
 
338
147
  if (!routeTable) {
339
148
  console.warn(`No route table found for subnet ${subnetId}`);
340
- return true; // Default to private for safety
149
+ return true;
341
150
  }
342
151
 
343
- // Check if route table has a route to an Internet Gateway
344
- let hasIgwRoute = false;
345
- let gatewayId = null;
346
-
347
- for (const route of routeTable.Routes || []) {
348
- if (route.GatewayId && route.GatewayId.startsWith('igw-')) {
349
- hasIgwRoute = true;
350
- gatewayId = route.GatewayId;
351
- break;
352
- }
353
- }
354
-
355
- // Enhanced logging for validation
356
- if (hasIgwRoute) {
152
+ const gatewayId = this._findIgwRoute(routeTable);
153
+ if (gatewayId) {
357
154
  console.log(
358
155
  `āœ… Subnet ${subnetId} is PUBLIC (has route to IGW ${gatewayId})`
359
156
  );
360
- return false; // It's a public subnet
361
- } else {
362
- console.log(
363
- `šŸ”’ Subnet ${subnetId} is PRIVATE (no IGW route found)`
364
- );
365
- return true; // No IGW route found, it's private
157
+ return false;
366
158
  }
159
+
160
+ console.log(
161
+ `šŸ”’ Subnet ${subnetId} is PRIVATE (no IGW route found)`
162
+ );
163
+ return true;
367
164
  } catch (error) {
368
165
  console.warn(
369
166
  `Could not determine if subnet ${subnetId} is private:`,
370
167
  error
371
168
  );
372
- return true; // Default to private for safety
169
+ return true;
373
170
  }
374
171
  }
375
172
 
376
- /**
377
- * Find or create a default security group for Lambda functions
378
- * @param {string} vpcId - The VPC ID to search within
379
- * @returns {Promise<Object>} Security group object containing GroupId and other properties
380
- * @throws {Error} If no security group is found for the VPC
381
- */
382
173
  async findDefaultSecurityGroup(vpcId) {
383
174
  try {
384
- // First try to find existing Frigg security group
385
- const friggSgCommand = new DescribeSecurityGroupsCommand({
386
- Filters: [
387
- {
388
- Name: 'vpc-id',
389
- Values: [vpcId],
390
- },
391
- {
392
- Name: 'group-name',
393
- Values: ['frigg-lambda-sg'],
394
- },
395
- ],
396
- });
397
-
398
- const friggResponse = await this.ec2Client.send(friggSgCommand);
399
- if (
400
- friggResponse.SecurityGroups &&
401
- friggResponse.SecurityGroups.length > 0
402
- ) {
403
- return friggResponse.SecurityGroups[0];
175
+ const friggGroup = await this._findSecurityGroupByName(
176
+ vpcId,
177
+ 'frigg-lambda-sg'
178
+ );
179
+ if (friggGroup) {
180
+ return friggGroup;
404
181
  }
405
182
 
406
- // Fall back to default security group
407
- const defaultSgCommand = new DescribeSecurityGroupsCommand({
408
- Filters: [
409
- {
410
- Name: 'vpc-id',
411
- Values: [vpcId],
412
- },
413
- {
414
- Name: 'group-name',
415
- Values: ['default'],
416
- },
417
- ],
418
- });
419
-
420
- const defaultResponse = await this.ec2Client.send(defaultSgCommand);
421
- if (
422
- defaultResponse.SecurityGroups &&
423
- defaultResponse.SecurityGroups.length > 0
424
- ) {
425
- return defaultResponse.SecurityGroups[0];
183
+ const defaultGroup = await this._findSecurityGroupByName(vpcId, 'default');
184
+ if (defaultGroup) {
185
+ return defaultGroup;
426
186
  }
427
187
 
428
188
  throw new Error(`No security group found for VPC ${vpcId}`);
@@ -432,42 +192,17 @@ class AWSDiscovery {
432
192
  }
433
193
  }
434
194
 
435
- /**
436
- * Find public subnets for NAT Gateway placement
437
- * @param {string} vpcId - The VPC ID to search within
438
- * @returns {Promise<Object>} First public subnet object for NAT Gateway placement
439
- * @throws {Error} If no public subnets are found in the VPC
440
- */
441
195
  async findPublicSubnets(vpcId) {
442
196
  try {
443
- const command = new DescribeSubnetsCommand({
444
- Filters: [
445
- {
446
- Name: 'vpc-id',
447
- Values: [vpcId],
448
- },
449
- ],
450
- });
451
-
452
- const response = await this.ec2Client.send(command);
197
+ const subnets = await this._fetchSubnets(vpcId);
453
198
 
454
- if (!response.Subnets || response.Subnets.length === 0) {
199
+ if (subnets.length === 0) {
455
200
  throw new Error(`No subnets found in VPC ${vpcId}`);
456
201
  }
457
202
 
458
- // Find public subnets (have direct route to IGW)
459
- const publicSubnets = [];
460
-
461
- for (const subnet of response.Subnets) {
462
- // Check route tables to determine if subnet is public
463
- const isPrivate = await this.isSubnetPrivate(subnet.SubnetId);
464
- if (!isPrivate) {
465
- publicSubnets.push(subnet);
466
- }
467
- }
203
+ const { publicSubnets } = await this._classifySubnets(subnets);
468
204
 
469
205
  if (publicSubnets.length === 0) {
470
- // If no public subnets found, we need to create one or inform the user
471
206
  console.warn(
472
207
  `WARNING: No public subnets found in VPC ${vpcId}`
473
208
  );
@@ -477,10 +212,9 @@ class AWSDiscovery {
477
212
  console.warn(
478
213
  'Please create a public subnet or use VPC endpoints instead'
479
214
  );
480
- return null; // Return null instead of throwing to allow graceful handling
215
+ return null;
481
216
  }
482
217
 
483
- // Return first public subnet for NAT Gateway
484
218
  console.log(
485
219
  `Found ${publicSubnets.length} public subnets, using ${publicSubnets[0].SubnetId} for NAT Gateway`
486
220
  );
@@ -491,56 +225,25 @@ class AWSDiscovery {
491
225
  }
492
226
  }
493
227
 
494
- /**
495
- * Find private route table for VPC endpoints
496
- * @param {string} vpcId - The VPC ID to search within
497
- * @returns {Promise<Object>} Route table object containing RouteTableId and other properties
498
- * @throws {Error} If no route tables are found for the VPC
499
- */
500
228
  async findPrivateRouteTable(vpcId) {
501
229
  try {
502
- const command = new DescribeRouteTablesCommand({
503
- Filters: [
504
- {
505
- Name: 'vpc-id',
506
- Values: [vpcId],
507
- },
508
- ],
509
- });
510
-
511
- const response = await this.ec2Client.send(command);
230
+ const routeTables = await this.findRouteTables(vpcId);
512
231
 
513
- if (!response.RouteTables || response.RouteTables.length === 0) {
232
+ if (routeTables.length === 0) {
514
233
  throw new Error(`No route tables found for VPC ${vpcId}`);
515
234
  }
516
235
 
517
- // Find a route table that doesn't have direct IGW route (private)
518
- for (const routeTable of response.RouteTables) {
519
- let hasIgwRoute = false;
520
- for (const route of routeTable.Routes || []) {
521
- if (route.GatewayId && route.GatewayId.startsWith('igw-')) {
522
- hasIgwRoute = true;
523
- break;
524
- }
525
- }
526
- if (!hasIgwRoute) {
527
- return routeTable;
528
- }
529
- }
236
+ const privateTable = routeTables.find(
237
+ (rt) => !this._findIgwRoute(rt)
238
+ );
530
239
 
531
- // If no private route table found, return the first one
532
- return response.RouteTables[0];
240
+ return privateTable || routeTables[0];
533
241
  } catch (error) {
534
242
  console.error('Error finding private route table:', error);
535
243
  throw error;
536
244
  }
537
245
  }
538
246
 
539
- /**
540
- * Find existing NAT Gateways in the VPC
541
- * @param {string} vpcId - The VPC ID to search within
542
- * @returns {Promise<Object|null>} NAT Gateway object or null if none found
543
- */
544
247
  async findExistingNatGateway(vpcId) {
545
248
  try {
546
249
  const command = new DescribeNatGatewaysCommand({
@@ -558,122 +261,85 @@ class AWSDiscovery {
558
261
 
559
262
  const response = await this.ec2Client.send(command);
560
263
 
561
- if (response.NatGateways && response.NatGateways.length > 0) {
562
- // Filter out any NAT Gateways that are not truly available
563
- const availableNatGateways = response.NatGateways.filter(nat => {
564
- // Double-check the state is truly 'available'
565
- if (nat.State !== 'available') {
566
- console.warn(`Skipping NAT Gateway ${nat.NatGatewayId} with state: ${nat.State}`);
567
- return false;
568
- }
569
- return true;
570
- });
571
-
572
- if (availableNatGateways.length === 0) {
573
- console.warn('No truly available NAT Gateways found in VPC');
574
- return null;
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;
575
270
  }
271
+ return true;
272
+ });
576
273
 
577
- // Sort NAT Gateways to prioritize Frigg-managed ones
578
- const sortedNatGateways = availableNatGateways.sort((a, b) => {
579
- const aIsFrigg =
580
- a.Tags &&
581
- a.Tags.some(
582
- (tag) =>
583
- (tag.Key === 'ManagedBy' &&
584
- tag.Value === 'Frigg') ||
585
- (tag.Key === 'Name' &&
586
- tag.Value.includes('frigg'))
587
- );
588
- const bIsFrigg =
589
- b.Tags &&
590
- b.Tags.some(
591
- (tag) =>
592
- (tag.Key === 'ManagedBy' &&
593
- tag.Value === 'Frigg') ||
594
- (tag.Key === 'Name' &&
595
- tag.Value.includes('frigg'))
596
- );
274
+ if (natGateways.length === 0) {
275
+ console.warn('No truly available NAT Gateways found in VPC');
276
+ return null;
277
+ }
597
278
 
598
- if (aIsFrigg && !bIsFrigg) return -1;
599
- if (!aIsFrigg && bIsFrigg) return 1;
600
- return 0;
601
- });
279
+ const sortedNatGateways = natGateways.sort((a, b) => {
280
+ const aIsFrigg = this._isFriggManaged(a.Tags);
281
+ const bIsFrigg = this._isFriggManaged(b.Tags);
602
282
 
603
- // Check each NAT Gateway to ensure it's properly configured
604
- for (const natGateway of sortedNatGateways) {
605
- const subnetId = natGateway.SubnetId;
606
- const isPrivate = await this.isSubnetPrivate(subnetId);
607
-
608
- // Check if it's a Frigg-managed NAT Gateway
609
- const isFriggNat =
610
- natGateway.Tags &&
611
- natGateway.Tags.some(
612
- (tag) =>
613
- (tag.Key === 'ManagedBy' &&
614
- tag.Value === 'Frigg') ||
615
- (tag.Key === 'Name' &&
616
- tag.Value.includes('frigg'))
617
- );
283
+ if (aIsFrigg && !bIsFrigg) return -1;
284
+ if (!aIsFrigg && bIsFrigg) return 1;
285
+ return 0;
286
+ });
618
287
 
619
- if (isPrivate) {
620
- // NAT Gateway appears to be in a private subnet
621
- // This could be due to route table misconfiguration
622
- console.warn(
623
- `WARNING: NAT Gateway ${natGateway.NatGatewayId} is in subnet ${subnetId} which appears to be private`
624
- );
288
+ for (const natGateway of sortedNatGateways) {
289
+ const subnetId = natGateway.SubnetId;
290
+ const isPrivate = await this.isSubnetPrivate(
291
+ subnetId,
292
+ natGateway.VpcId
293
+ );
294
+ const isFriggNat = this._isFriggManaged(natGateway.Tags);
625
295
 
626
- if (isFriggNat) {
627
- console.warn(
628
- 'This is a Frigg-managed NAT Gateway that may have been misconfigured by route table changes'
629
- );
630
- console.warn(
631
- 'Consider enabling selfHeal: true to fix this automatically'
632
- );
633
- // Return it anyway if it's Frigg-managed - we can fix the routes
634
- // Mark that it's in a private subnet
635
- natGateway._isInPrivateSubnet = true;
636
- return natGateway;
637
- } else {
638
- console.warn(
639
- 'NAT Gateways MUST be placed in public subnets with Internet Gateway routes'
640
- );
641
- console.warn(
642
- 'Skipping this misconfigured NAT Gateway...'
643
- );
644
- continue; // Skip non-Frigg NAT Gateways in private subnets
645
- }
646
- }
296
+ if (isPrivate) {
297
+ console.warn(
298
+ `WARNING: NAT Gateway ${natGateway.NatGatewayId} is in subnet ${subnetId} which appears to be private`
299
+ );
647
300
 
648
301
  if (isFriggNat) {
649
- console.log(
650
- `Found existing Frigg-managed NAT Gateway: ${natGateway.NatGatewayId} (State: ${natGateway.State})`
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'
651
307
  );
652
- natGateway._isInPrivateSubnet = false;
308
+ natGateway._isInPrivateSubnet = true;
653
309
  return natGateway;
654
310
  }
655
311
 
656
- // Return first valid NAT Gateway that's in a public subnet
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;
317
+ }
318
+
319
+ if (isFriggNat) {
657
320
  console.log(
658
- `Found existing NAT Gateway in public subnet: ${natGateway.NatGatewayId} (State: ${natGateway.State})`
321
+ `Found existing Frigg-managed NAT Gateway: ${natGateway.NatGatewayId} (State: ${natGateway.State})`
659
322
  );
660
323
  natGateway._isInPrivateSubnet = false;
661
324
  return natGateway;
662
325
  }
663
326
 
664
- // All non-Frigg NAT Gateways are in private subnets
665
- console.error(
666
- `ERROR: Found ${response.NatGateways.length} NAT Gateway(s) but all non-Frigg ones are in private subnets!`
667
- );
668
- console.error(
669
- 'These NAT Gateways will not provide internet connectivity without route table fixes'
670
- );
671
- console.error(
672
- 'Enable selfHeal: true to fix automatically or create a new NAT Gateway'
327
+ console.log(
328
+ `Found existing NAT Gateway in public subnet: ${natGateway.NatGatewayId} (State: ${natGateway.State})`
673
329
  );
674
- return null; // Return null to trigger creation of new NAT Gateway
330
+ natGateway._isInPrivateSubnet = false;
331
+ return natGateway;
675
332
  }
676
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
+ );
677
343
  return null;
678
344
  } catch (error) {
679
345
  console.warn('Error finding existing NAT Gateway:', error.message);
@@ -681,17 +347,12 @@ class AWSDiscovery {
681
347
  }
682
348
  }
683
349
 
684
- /**
685
- * Find available Elastic IPs
686
- * @returns {Promise<Object|null>} Available EIP object or null if none found
687
- */
688
350
  async findAvailableElasticIP() {
689
351
  try {
690
352
  const command = new DescribeAddressesCommand({});
691
353
  const response = await this.ec2Client.send(command);
692
354
 
693
355
  if (response.Addresses && response.Addresses.length > 0) {
694
- // Find an unassociated EIP first
695
356
  const availableEIP = response.Addresses.find(
696
357
  (eip) =>
697
358
  !eip.AssociationId &&
@@ -706,15 +367,8 @@ class AWSDiscovery {
706
367
  return availableEIP;
707
368
  }
708
369
 
709
- // Check for EIPs tagged for Frigg
710
- const friggEIP = response.Addresses.find(
711
- (eip) =>
712
- eip.Tags &&
713
- eip.Tags.some(
714
- (tag) =>
715
- tag.Key === 'Name' &&
716
- tag.Value.includes('frigg')
717
- )
370
+ const friggEIP = response.Addresses.find((eip) =>
371
+ this._isFriggManaged(eip.Tags)
718
372
  );
719
373
 
720
374
  if (friggEIP) {
@@ -732,14 +386,9 @@ class AWSDiscovery {
732
386
  }
733
387
  }
734
388
 
735
- /**
736
- * Find the default KMS key for the account
737
- * @returns {Promise<string|null>} KMS key ARN or null if no key found
738
- */
739
389
  async findDefaultKmsKey() {
740
390
  console.log('KMS Discovery Starting...');
741
391
  try {
742
- // Log AWS account and region info for verification
743
392
  console.log(`[KMS Discovery] Running in region: ${this.region}`);
744
393
  try {
745
394
  const accountId = await this.getAccountId();
@@ -767,7 +416,6 @@ class AWSDiscovery {
767
416
  let enabledKeys = 0;
768
417
  let pendingDeletionKeys = 0;
769
418
 
770
- // Look for customer managed keys first
771
419
  for (const key of response.Keys) {
772
420
  try {
773
421
  const describeCommand = new DescribeKeyCommand({
@@ -778,58 +426,50 @@ class AWSDiscovery {
778
426
  );
779
427
  keysExamined++;
780
428
 
781
- if (keyDetails.KeyMetadata) {
782
- const metadata = keyDetails.KeyMetadata;
783
-
784
- // Log detailed key information
785
- console.log(`[KMS Discovery] Key ${key.KeyId}:`, {
786
- KeyManager: metadata.KeyManager,
787
- KeyState: metadata.KeyState,
788
- Enabled: metadata.Enabled,
789
- DeletionDate:
790
- metadata.DeletionDate ||
791
- 'Not scheduled for deletion',
792
- Arn: metadata.Arn,
793
- });
794
-
795
- if (metadata.KeyManager === 'CUSTOMER') {
796
- customerManagedKeys++;
797
-
798
- if (metadata.KeyState === 'Enabled') {
799
- enabledKeys++;
800
- } else if (
801
- metadata.KeyState === 'PendingDeletion'
802
- ) {
803
- pendingDeletionKeys++;
804
- console.warn(
805
- `[KMS Discovery] Skipping key ${key.KeyId} - State: PendingDeletion, DeletionDate: ${metadata.DeletionDate}`
806
- );
807
- }
808
-
809
- // Explicitly check for enabled state AND absence of deletion
810
- if (
811
- metadata.KeyManager === 'CUSTOMER' &&
812
- metadata.KeyState === 'Enabled' &&
813
- !metadata.DeletionDate
814
- ) {
815
- console.log(
816
- `[KMS Discovery] Found eligible customer managed KMS key: ${metadata.Arn}`
817
- );
818
- return metadata.Arn;
819
- } else if (
820
- metadata.KeyManager === 'CUSTOMER' &&
821
- metadata.KeyState === 'Enabled' &&
822
- metadata.DeletionDate
823
- ) {
824
- // This shouldn't happen according to AWS docs, but log it if it does
825
- console.error(
826
- `[KMS Discovery] WARNING: Key ${key.KeyId} has KeyState='Enabled' but DeletionDate is set: ${metadata.DeletionDate}`
827
- );
828
- }
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
+ );
829
470
  }
830
471
  }
831
472
  } catch (error) {
832
- // Continue to next key if we can't describe this one
833
473
  console.warn(
834
474
  `[KMS Discovery] Could not describe key ${key.KeyId}:`,
835
475
  error.message
@@ -838,13 +478,12 @@ class AWSDiscovery {
838
478
  }
839
479
  }
840
480
 
841
- // Summary logging
842
481
  console.log('[KMS Discovery] Summary:', {
843
482
  totalKeys: response.Keys.length,
844
- keysExamined: keysExamined,
845
- customerManagedKeys: customerManagedKeys,
846
- enabledKeys: enabledKeys,
847
- pendingDeletionKeys: pendingDeletionKeys,
483
+ keysExamined,
484
+ customerManagedKeys,
485
+ enabledKeys,
486
+ pendingDeletionKeys,
848
487
  });
849
488
 
850
489
  if (customerManagedKeys === 0) {
@@ -871,89 +510,6 @@ class AWSDiscovery {
871
510
  }
872
511
  }
873
512
 
874
- /**
875
- * Find Frigg-managed resources by tags
876
- * @param {string} vpcId - The VPC ID to search within
877
- * @returns {Promise<Object>} Object containing Frigg-managed resources
878
- */
879
- async findFriggManagedResources(vpcId) {
880
- try {
881
- const resources = {
882
- natGateways: [],
883
- elasticIps: [],
884
- subnets: [],
885
- routeTables: [],
886
- };
887
-
888
- // Find NAT Gateways with Frigg tags
889
- const natCommand = new DescribeNatGatewaysCommand({
890
- Filter: [
891
- { Name: 'vpc-id', Values: [vpcId] },
892
- { Name: 'state', Values: ['available'] },
893
- ],
894
- });
895
- const natResponse = await this.ec2Client.send(natCommand);
896
-
897
- if (natResponse.NatGateways) {
898
- resources.natGateways = natResponse.NatGateways.filter(
899
- (nat) =>
900
- nat.Tags &&
901
- nat.Tags.some(
902
- (tag) =>
903
- tag.Key === 'ManagedBy' && tag.Value === 'Frigg'
904
- )
905
- );
906
- }
907
-
908
- // Find Elastic IPs with Frigg tags
909
- const eipCommand = new DescribeAddressesCommand({});
910
- const eipResponse = await this.ec2Client.send(eipCommand);
911
-
912
- if (eipResponse.Addresses) {
913
- resources.elasticIps = eipResponse.Addresses.filter(
914
- (eip) =>
915
- eip.Tags &&
916
- eip.Tags.some(
917
- (tag) =>
918
- tag.Key === 'ManagedBy' && tag.Value === 'Frigg'
919
- )
920
- );
921
- }
922
-
923
- // Find Route Tables with Frigg tags
924
- const rtCommand = new DescribeRouteTablesCommand({
925
- Filters: [{ Name: 'vpc-id', Values: [vpcId] }],
926
- });
927
- const rtResponse = await this.ec2Client.send(rtCommand);
928
-
929
- if (rtResponse.RouteTables) {
930
- resources.routeTables = rtResponse.RouteTables.filter(
931
- (rt) =>
932
- rt.Tags &&
933
- rt.Tags.some(
934
- (tag) =>
935
- tag.Key === 'ManagedBy' && tag.Value === 'Frigg'
936
- )
937
- );
938
- }
939
-
940
- return resources;
941
- } catch (error) {
942
- console.error('Error finding Frigg-managed resources:', error);
943
- return {
944
- natGateways: [],
945
- elasticIps: [],
946
- subnets: [],
947
- routeTables: [],
948
- };
949
- }
950
- }
951
-
952
- /**
953
- * Detect misconfigured resources that need healing
954
- * @param {string} vpcId - The VPC ID to check
955
- * @returns {Promise<Object>} Object containing misconfiguration details
956
- */
957
513
  async detectMisconfiguredResources(vpcId) {
958
514
  try {
959
515
  const misconfigurations = {
@@ -963,7 +519,6 @@ class AWSDiscovery {
963
519
  privateSubnetsWithoutNatRoute: [],
964
520
  };
965
521
 
966
- // Find NAT Gateways in private subnets
967
522
  const natCommand = new DescribeNatGatewaysCommand({
968
523
  Filter: [
969
524
  { Name: 'vpc-id', Values: [vpcId] },
@@ -972,75 +527,52 @@ class AWSDiscovery {
972
527
  });
973
528
  const natResponse = await this.ec2Client.send(natCommand);
974
529
 
975
- if (natResponse.NatGateways) {
976
- for (const nat of natResponse.NatGateways) {
977
- const isPrivate = await this.isSubnetPrivate(nat.SubnetId);
978
- if (isPrivate) {
979
- misconfigurations.natGatewaysInPrivateSubnets.push({
980
- natGatewayId: nat.NatGatewayId,
981
- subnetId: nat.SubnetId,
982
- tags: nat.Tags,
983
- });
984
- }
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
+ });
985
538
  }
986
539
  }
987
540
 
988
- // Find orphaned Elastic IPs
989
541
  const eipCommand = new DescribeAddressesCommand({});
990
542
  const eipResponse = await this.ec2Client.send(eipCommand);
991
543
 
992
- if (eipResponse.Addresses) {
993
- for (const eip of eipResponse.Addresses) {
994
- if (
995
- !eip.InstanceId &&
996
- !eip.NetworkInterfaceId &&
997
- !eip.AssociationId
998
- ) {
999
- // Check if it's Frigg-managed
1000
- const isFriggManaged =
1001
- eip.Tags &&
1002
- eip.Tags.some(
1003
- (tag) =>
1004
- tag.Key === 'ManagedBy' &&
1005
- tag.Value === 'Frigg'
1006
- );
1007
- if (isFriggManaged) {
1008
- misconfigurations.orphanedElasticIps.push({
1009
- allocationId: eip.AllocationId,
1010
- publicIp: eip.PublicIp,
1011
- tags: eip.Tags,
1012
- });
1013
- }
1014
- }
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
+ });
1015
556
  }
1016
557
  }
1017
558
 
1018
- // Find private subnets without NAT route
1019
559
  const subnets = await this.findPrivateSubnets(vpcId);
1020
560
  const routeTables = await this.findRouteTables(vpcId);
1021
561
 
1022
562
  for (const subnet of subnets) {
1023
- let hasNatRoute = false;
1024
-
1025
- // Find route table for this subnet
1026
- for (const rt of routeTables) {
1027
- const isAssociated =
1028
- rt.Associations &&
1029
- rt.Associations.some(
1030
- (assoc) => assoc.SubnetId === subnet.SubnetId
1031
- );
1032
-
1033
- if (isAssociated) {
1034
- hasNatRoute =
1035
- rt.Routes &&
1036
- rt.Routes.some(
1037
- (route) =>
1038
- route.NatGatewayId &&
1039
- route.DestinationCidrBlock === '0.0.0.0/0'
1040
- );
1041
- break;
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;
1042
569
  }
1043
- }
570
+ return (rt.Routes || []).some(
571
+ (route) =>
572
+ route.NatGatewayId &&
573
+ route.DestinationCidrBlock === '0.0.0.0/0'
574
+ );
575
+ });
1044
576
 
1045
577
  if (!hasNatRoute) {
1046
578
  misconfigurations.privateSubnetsWithoutNatRoute.push({
@@ -1062,11 +594,6 @@ class AWSDiscovery {
1062
594
  }
1063
595
  }
1064
596
 
1065
- /**
1066
- * Get healing recommendations based on detected issues
1067
- * @param {Object} misconfigurations - Object from detectMisconfiguredResources
1068
- * @returns {Array} Array of healing recommendations
1069
- */
1070
597
  getHealingRecommendations(misconfigurations) {
1071
598
  const recommendations = [];
1072
599
 
@@ -1107,7 +634,6 @@ class AWSDiscovery {
1107
634
  });
1108
635
  }
1109
636
 
1110
- // Sort by severity
1111
637
  recommendations.sort((a, b) => {
1112
638
  const severityOrder = { critical: 0, warning: 1, info: 2 };
1113
639
  return severityOrder[a.severity] - severityOrder[b.severity];
@@ -1116,18 +642,6 @@ class AWSDiscovery {
1116
642
  return recommendations;
1117
643
  }
1118
644
 
1119
- /**
1120
- * Discover all AWS resources needed for Frigg deployment
1121
- * @returns {Promise<Object>} Object containing discovered resource IDs:
1122
- * @returns {string} return.defaultVpcId - The default VPC ID
1123
- * @returns {string} return.defaultSecurityGroupId - The default security group ID
1124
- * @returns {string} return.privateSubnetId1 - First private subnet ID
1125
- * @returns {string} return.privateSubnetId2 - Second private subnet ID
1126
- * @returns {string} return.publicSubnetId - Public subnet ID for NAT Gateway
1127
- * @returns {string} return.privateRouteTableId - Private route table ID
1128
- * @returns {string|null} return.defaultKmsKeyId - Default KMS key ARN or null if not found
1129
- * @throws {Error} If resource discovery fails
1130
- */
1131
645
  async discoverResources(options = {}) {
1132
646
  try {
1133
647
  console.log(
@@ -1138,7 +652,6 @@ class AWSDiscovery {
1138
652
  const vpc = await this.findDefaultVpc();
1139
653
  console.log(`\nāœ… Found VPC: ${vpc.VpcId}`);
1140
654
 
1141
- // Enable auto-convert if selfHeal is enabled
1142
655
  const autoConvert = options.selfHeal || false;
1143
656
 
1144
657
  const privateSubnets = await this.findPrivateSubnets(
@@ -1177,7 +690,6 @@ class AWSDiscovery {
1177
690
  console.log('ā„¹ļø No KMS key found');
1178
691
  }
1179
692
 
1180
- // Try to find existing NAT Gateway
1181
693
  const existingNatGateway = await this.findExistingNatGateway(
1182
694
  vpc.VpcId
1183
695
  );
@@ -1187,11 +699,9 @@ class AWSDiscovery {
1187
699
 
1188
700
  if (existingNatGateway) {
1189
701
  natGatewayId = existingNatGateway.NatGatewayId;
1190
- // Check if NAT Gateway is in a private subnet (from our detection)
1191
702
  natGatewayInPrivateSubnet =
1192
703
  existingNatGateway._isInPrivateSubnet || false;
1193
704
 
1194
- // Get the EIP allocation ID from the NAT Gateway
1195
705
  if (
1196
706
  existingNatGateway.NatGatewayAddresses &&
1197
707
  existingNatGateway.NatGatewayAddresses.length > 0
@@ -1200,19 +710,23 @@ class AWSDiscovery {
1200
710
  existingNatGateway.NatGatewayAddresses[0].AllocationId;
1201
711
  }
1202
712
  } else {
1203
- // If no NAT Gateway exists, check for available EIP
1204
713
  const availableEIP = await this.findAvailableElasticIP();
1205
714
  if (availableEIP) {
1206
715
  elasticIpAllocationId = availableEIP.AllocationId;
1207
716
  }
1208
717
  }
1209
718
 
1210
- // Check if the "private" subnets are actually public
1211
719
  const subnet1IsActuallyPrivate = privateSubnets[0]
1212
- ? await this.isSubnetPrivate(privateSubnets[0].SubnetId)
720
+ ? await this.isSubnetPrivate(
721
+ privateSubnets[0].SubnetId,
722
+ privateSubnets[0].VpcId || vpc.VpcId
723
+ )
1213
724
  : false;
1214
725
  const subnet2IsActuallyPrivate = privateSubnets[1]
1215
- ? await this.isSubnetPrivate(privateSubnets[1].SubnetId)
726
+ ? await this.isSubnetPrivate(
727
+ privateSubnets[1].SubnetId,
728
+ privateSubnets[1].VpcId || vpc.VpcId
729
+ )
1216
730
  : subnet1IsActuallyPrivate;
1217
731
 
1218
732
  const subnetStatus = {
@@ -1267,12 +781,12 @@ class AWSDiscovery {
1267
781
 
1268
782
  return {
1269
783
  defaultVpcId: vpc.VpcId,
1270
- vpcCidr: vpc.CidrBlock, // Add VPC CIDR for security group configuration
784
+ vpcCidr: vpc.CidrBlock,
1271
785
  defaultSecurityGroupId: securityGroup.GroupId,
1272
786
  privateSubnetId1: privateSubnets[0]?.SubnetId,
1273
787
  privateSubnetId2:
1274
788
  privateSubnets[1]?.SubnetId || privateSubnets[0]?.SubnetId,
1275
- publicSubnetId: publicSubnet?.SubnetId || null, // May be null if no public subnet exists
789
+ publicSubnetId: publicSubnet?.SubnetId || null,
1276
790
  privateRouteTableId: routeTable.RouteTableId,
1277
791
  defaultKmsKeyId: kmsKeyArn,
1278
792
  existingNatGatewayId: natGatewayId,
@@ -1302,11 +816,6 @@ class AWSDiscovery {
1302
816
  }
1303
817
  }
1304
818
 
1305
- /**
1306
- * Find an existing Internet Gateway attached to the VPC
1307
- * @param {string} vpcId - VPC ID to search in
1308
- * @returns {Promise<Object|null>} Internet Gateway object or null if none found
1309
- */
1310
819
  async findInternetGateway(vpcId) {
1311
820
  try {
1312
821
  const command = new DescribeInternetGatewaysCommand({
@@ -1341,24 +850,17 @@ class AWSDiscovery {
1341
850
  }
1342
851
  }
1343
852
 
1344
- /**
1345
- * Find Frigg-managed resources by tags
1346
- * @param {string} serviceName - The service name to search for
1347
- * @param {string} stage - The deployment stage
1348
- * @returns {Promise<Object>} Object containing found Frigg-managed resources
1349
- */
1350
853
  async findFriggManagedResources(serviceName, stage) {
1351
- try {
1352
- const results = {
1353
- natGateways: [],
1354
- elasticIps: [],
1355
- routeTables: [],
1356
- subnets: [],
1357
- securityGroups: [],
1358
- };
854
+ const results = {
855
+ natGateways: [],
856
+ elasticIps: [],
857
+ routeTables: [],
858
+ subnets: [],
859
+ securityGroups: [],
860
+ };
1359
861
 
1360
- // Common filter for Frigg-managed resources
1361
- const friggFilters = [
862
+ try {
863
+ const filters = [
1362
864
  {
1363
865
  Name: 'tag:ManagedBy',
1364
866
  Values: ['Frigg'],
@@ -1366,82 +868,76 @@ class AWSDiscovery {
1366
868
  ];
1367
869
 
1368
870
  if (serviceName) {
1369
- friggFilters.push({
871
+ filters.push({
1370
872
  Name: 'tag:Service',
1371
873
  Values: [serviceName],
1372
874
  });
1373
875
  }
1374
876
 
1375
877
  if (stage) {
1376
- friggFilters.push({
878
+ filters.push({
1377
879
  Name: 'tag:Stage',
1378
880
  Values: [stage],
1379
881
  });
1380
882
  }
1381
883
 
1382
- // Find NAT Gateways
1383
- try {
1384
- const natCommand = new DescribeNatGatewaysCommand({
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
+ {
1385
902
  Filter: [
1386
- ...friggFilters,
903
+ ...filters,
1387
904
  {
1388
905
  Name: 'state',
1389
906
  Values: ['available'],
1390
907
  },
1391
908
  ],
1392
- });
1393
- const natResponse = await this.ec2Client.send(natCommand);
1394
- results.natGateways = natResponse.NatGateways || [];
1395
- } catch (err) {
1396
- console.warn('Error finding Frigg NAT Gateways:', err.message);
1397
- }
909
+ },
910
+ 'NatGateways',
911
+ 'NAT Gateways'
912
+ );
1398
913
 
1399
- // Find Elastic IPs
1400
- try {
1401
- const eipCommand = new DescribeAddressesCommand({
1402
- Filters: friggFilters,
1403
- });
1404
- const eipResponse = await this.ec2Client.send(eipCommand);
1405
- results.elasticIps = eipResponse.Addresses || [];
1406
- } catch (err) {
1407
- console.warn('Error finding Frigg Elastic IPs:', err.message);
1408
- }
914
+ results.elasticIps = await fetchWithFallback(
915
+ DescribeAddressesCommand,
916
+ { Filters: filters },
917
+ 'Addresses',
918
+ 'Elastic IPs'
919
+ );
1409
920
 
1410
- // Find Route Tables
1411
- try {
1412
- const rtCommand = new DescribeRouteTablesCommand({
1413
- Filters: friggFilters,
1414
- });
1415
- const rtResponse = await this.ec2Client.send(rtCommand);
1416
- results.routeTables = rtResponse.RouteTables || [];
1417
- } catch (err) {
1418
- console.warn('Error finding Frigg Route Tables:', err.message);
1419
- }
921
+ results.routeTables = await fetchWithFallback(
922
+ DescribeRouteTablesCommand,
923
+ { Filters: filters },
924
+ 'RouteTables',
925
+ 'Route Tables'
926
+ );
1420
927
 
1421
- // Find Subnets
1422
- try {
1423
- const subnetCommand = new DescribeSubnetsCommand({
1424
- Filters: friggFilters,
1425
- });
1426
- const subnetResponse = await this.ec2Client.send(subnetCommand);
1427
- results.subnets = subnetResponse.Subnets || [];
1428
- } catch (err) {
1429
- console.warn('Error finding Frigg Subnets:', err.message);
1430
- }
928
+ results.subnets = await fetchWithFallback(
929
+ DescribeSubnetsCommand,
930
+ { Filters: filters },
931
+ 'Subnets',
932
+ 'Subnets'
933
+ );
1431
934
 
1432
- // Find Security Groups
1433
- try {
1434
- const sgCommand = new DescribeSecurityGroupsCommand({
1435
- Filters: friggFilters,
1436
- });
1437
- const sgResponse = await this.ec2Client.send(sgCommand);
1438
- results.securityGroups = sgResponse.SecurityGroups || [];
1439
- } catch (err) {
1440
- console.warn(
1441
- 'Error finding Frigg Security Groups:',
1442
- err.message
1443
- );
1444
- }
935
+ results.securityGroups = await fetchWithFallback(
936
+ DescribeSecurityGroupsCommand,
937
+ { Filters: filters },
938
+ 'SecurityGroups',
939
+ 'Security Groups'
940
+ );
1445
941
 
1446
942
  console.log('Found Frigg-managed resources:', {
1447
943
  natGateways: results.natGateways.length,
@@ -1454,14 +950,226 @@ class AWSDiscovery {
1454
950
  return results;
1455
951
  } catch (error) {
1456
952
  console.error('Error finding Frigg-managed resources:', error);
1457
- return {
1458
- natGateways: [],
1459
- elasticIps: [],
1460
- routeTables: [],
1461
- subnets: [],
1462
- securityGroups: [],
1463
- };
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
+ }
1464
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;
1465
1173
  }
1466
1174
  }
1467
1175