@friggframework/devtools 2.0.0--canary.428.3bab734.0 → 2.0.0--canary.428.08c3f6f.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.
@@ -147,6 +147,16 @@ class AWSDiscovery {
147
147
  * @param {string} subnetId - The subnet ID to check
148
148
  * @returns {Promise<boolean>} True if subnet is private, false if public
149
149
  */
150
+ /**
151
+ * Validate if a subnet is truly public (has IGW route)
152
+ * @param {string} subnetId - The subnet ID to validate
153
+ * @returns {Promise<boolean>} true if public (has IGW route), false if private
154
+ */
155
+ async isSubnetPublic(subnetId) {
156
+ const isPrivate = await this.isSubnetPrivate(subnetId);
157
+ return !isPrivate;
158
+ }
159
+
150
160
  async isSubnetPrivate(subnetId) {
151
161
  try {
152
162
  // First, get the subnet details to find its VPC
@@ -207,13 +217,25 @@ class AWSDiscovery {
207
217
  }
208
218
 
209
219
  // Check if route table has a route to an Internet Gateway
220
+ let hasIgwRoute = false;
221
+ let gatewayId = null;
222
+
210
223
  for (const route of routeTable.Routes || []) {
211
224
  if (route.GatewayId && route.GatewayId.startsWith('igw-')) {
212
- return false; // It's a public subnet
225
+ hasIgwRoute = true;
226
+ gatewayId = route.GatewayId;
227
+ break;
213
228
  }
214
229
  }
215
-
216
- return true; // No IGW route found, it's private
230
+
231
+ // Enhanced logging for validation
232
+ if (hasIgwRoute) {
233
+ console.log(`✅ Subnet ${subnetId} is PUBLIC (has route to IGW ${gatewayId})`);
234
+ return false; // It's a public subnet
235
+ } else {
236
+ console.log(`🔒 Subnet ${subnetId} is PRIVATE (no IGW route found)`);
237
+ return true; // No IGW route found, it's private
238
+ }
217
239
  } catch (error) {
218
240
  console.warn(`Could not determine if subnet ${subnetId} is private:`, error);
219
241
  return true; // Default to private for safety
@@ -392,37 +414,64 @@ class AWSDiscovery {
392
414
  const response = await this.ec2Client.send(command);
393
415
 
394
416
  if (response.NatGateways && response.NatGateways.length > 0) {
395
- // Check each NAT Gateway to ensure it's in a public subnet
396
- for (const natGateway of response.NatGateways) {
417
+ // Sort NAT Gateways to prioritize Frigg-managed ones
418
+ const sortedNatGateways = response.NatGateways.sort((a, b) => {
419
+ const aIsFrigg = a.Tags && a.Tags.some(tag =>
420
+ (tag.Key === 'ManagedBy' && tag.Value === 'Frigg') ||
421
+ (tag.Key === 'Name' && tag.Value.includes('frigg'))
422
+ );
423
+ const bIsFrigg = b.Tags && b.Tags.some(tag =>
424
+ (tag.Key === 'ManagedBy' && tag.Value === 'Frigg') ||
425
+ (tag.Key === 'Name' && tag.Value.includes('frigg'))
426
+ );
427
+
428
+ if (aIsFrigg && !bIsFrigg) return -1;
429
+ if (!aIsFrigg && bIsFrigg) return 1;
430
+ return 0;
431
+ });
432
+
433
+ // Check each NAT Gateway to ensure it's properly configured
434
+ for (const natGateway of sortedNatGateways) {
397
435
  const subnetId = natGateway.SubnetId;
398
436
  const isPrivate = await this.isSubnetPrivate(subnetId);
399
437
 
400
- if (isPrivate) {
401
- console.warn(`WARNING: NAT Gateway ${natGateway.NatGatewayId} is in private subnet ${subnetId} - this will not work!`);
402
- console.warn('NAT Gateways MUST be placed in public subnets with Internet Gateway routes');
403
- console.warn('Skipping this misconfigured NAT Gateway...');
404
- continue; // Skip this NAT Gateway
405
- }
406
-
407
- // Check if it's a Frigg-tagged NAT Gateway
438
+ // Check if it's a Frigg-managed NAT Gateway
408
439
  const isFriggNat = natGateway.Tags && natGateway.Tags.some(tag =>
409
- tag.Key === 'Name' && tag.Value.includes('frigg')
440
+ (tag.Key === 'ManagedBy' && tag.Value === 'Frigg') ||
441
+ (tag.Key === 'Name' && tag.Value.includes('frigg'))
410
442
  );
411
443
 
444
+ if (isPrivate) {
445
+ // NAT Gateway appears to be in a private subnet
446
+ // This could be due to route table misconfiguration
447
+ console.warn(`WARNING: NAT Gateway ${natGateway.NatGatewayId} is in subnet ${subnetId} which appears to be private`);
448
+
449
+ if (isFriggNat) {
450
+ console.warn('This is a Frigg-managed NAT Gateway that may have been misconfigured by route table changes');
451
+ console.warn('Consider enabling selfHeal: true to fix this automatically');
452
+ // Return it anyway if it's Frigg-managed - we can fix the routes
453
+ return natGateway;
454
+ } else {
455
+ console.warn('NAT Gateways MUST be placed in public subnets with Internet Gateway routes');
456
+ console.warn('Skipping this misconfigured NAT Gateway...');
457
+ continue; // Skip non-Frigg NAT Gateways in private subnets
458
+ }
459
+ }
460
+
412
461
  if (isFriggNat) {
413
- console.log(`Found existing Frigg NAT Gateway in public subnet: ${natGateway.NatGatewayId}`);
462
+ console.log(`Found existing Frigg-managed NAT Gateway: ${natGateway.NatGatewayId}`);
414
463
  return natGateway;
415
464
  }
416
465
 
417
- // Keep track of first valid NAT Gateway as fallback
466
+ // Return first valid NAT Gateway that's in a public subnet
418
467
  console.log(`Found existing NAT Gateway in public subnet: ${natGateway.NatGatewayId}`);
419
- return natGateway; // Return first NAT Gateway that's in a public subnet
468
+ return natGateway;
420
469
  }
421
470
 
422
- // All NAT Gateways are in private subnets - don't use any of them
423
- console.error(`ERROR: Found ${response.NatGateways.length} NAT Gateway(s) but all are in private subnets!`);
424
- console.error('These NAT Gateways will not provide internet connectivity');
425
- console.error('A new NAT Gateway will be created in a public subnet');
471
+ // All non-Frigg NAT Gateways are in private subnets
472
+ console.error(`ERROR: Found ${response.NatGateways.length} NAT Gateway(s) but all non-Frigg ones are in private subnets!`);
473
+ console.error('These NAT Gateways will not provide internet connectivity without route table fixes');
474
+ console.error('Enable selfHeal: true to fix automatically or create a new NAT Gateway');
426
475
  return null; // Return null to trigger creation of new NAT Gateway
427
476
  }
428
477
 
@@ -514,6 +563,221 @@ class AWSDiscovery {
514
563
  }
515
564
  }
516
565
 
566
+ /**
567
+ * Find Frigg-managed resources by tags
568
+ * @param {string} vpcId - The VPC ID to search within
569
+ * @returns {Promise<Object>} Object containing Frigg-managed resources
570
+ */
571
+ async findFriggManagedResources(vpcId) {
572
+ try {
573
+ const resources = {
574
+ natGateways: [],
575
+ elasticIps: [],
576
+ subnets: [],
577
+ routeTables: []
578
+ };
579
+
580
+ // Find NAT Gateways with Frigg tags
581
+ const natCommand = new DescribeNatGatewaysCommand({
582
+ Filter: [
583
+ { Name: 'vpc-id', Values: [vpcId] },
584
+ { Name: 'state', Values: ['available'] }
585
+ ]
586
+ });
587
+ const natResponse = await this.ec2Client.send(natCommand);
588
+
589
+ if (natResponse.NatGateways) {
590
+ resources.natGateways = natResponse.NatGateways.filter(nat =>
591
+ nat.Tags && nat.Tags.some(tag =>
592
+ tag.Key === 'ManagedBy' && tag.Value === 'Frigg'
593
+ )
594
+ );
595
+ }
596
+
597
+ // Find Elastic IPs with Frigg tags
598
+ const eipCommand = new DescribeAddressesCommand({});
599
+ const eipResponse = await this.ec2Client.send(eipCommand);
600
+
601
+ if (eipResponse.Addresses) {
602
+ resources.elasticIps = eipResponse.Addresses.filter(eip =>
603
+ eip.Tags && eip.Tags.some(tag =>
604
+ tag.Key === 'ManagedBy' && tag.Value === 'Frigg'
605
+ )
606
+ );
607
+ }
608
+
609
+ // Find Route Tables with Frigg tags
610
+ const rtCommand = new DescribeRouteTablesCommand({
611
+ Filters: [
612
+ { Name: 'vpc-id', Values: [vpcId] }
613
+ ]
614
+ });
615
+ const rtResponse = await this.ec2Client.send(rtCommand);
616
+
617
+ if (rtResponse.RouteTables) {
618
+ resources.routeTables = rtResponse.RouteTables.filter(rt =>
619
+ rt.Tags && rt.Tags.some(tag =>
620
+ tag.Key === 'ManagedBy' && tag.Value === 'Frigg'
621
+ )
622
+ );
623
+ }
624
+
625
+ return resources;
626
+ } catch (error) {
627
+ console.error('Error finding Frigg-managed resources:', error);
628
+ return {
629
+ natGateways: [],
630
+ elasticIps: [],
631
+ subnets: [],
632
+ routeTables: []
633
+ };
634
+ }
635
+ }
636
+
637
+ /**
638
+ * Detect misconfigured resources that need healing
639
+ * @param {string} vpcId - The VPC ID to check
640
+ * @returns {Promise<Object>} Object containing misconfiguration details
641
+ */
642
+ async detectMisconfiguredResources(vpcId) {
643
+ try {
644
+ const misconfigurations = {
645
+ natGatewaysInPrivateSubnets: [],
646
+ orphanedElasticIps: [],
647
+ misconfiguredRouteTables: [],
648
+ privateSubnetsWithoutNatRoute: []
649
+ };
650
+
651
+ // Find NAT Gateways in private subnets
652
+ const natCommand = new DescribeNatGatewaysCommand({
653
+ Filter: [
654
+ { Name: 'vpc-id', Values: [vpcId] },
655
+ { Name: 'state', Values: ['available'] }
656
+ ]
657
+ });
658
+ const natResponse = await this.ec2Client.send(natCommand);
659
+
660
+ if (natResponse.NatGateways) {
661
+ for (const nat of natResponse.NatGateways) {
662
+ const isPrivate = await this.isSubnetPrivate(nat.SubnetId);
663
+ if (isPrivate) {
664
+ misconfigurations.natGatewaysInPrivateSubnets.push({
665
+ natGatewayId: nat.NatGatewayId,
666
+ subnetId: nat.SubnetId,
667
+ tags: nat.Tags
668
+ });
669
+ }
670
+ }
671
+ }
672
+
673
+ // Find orphaned Elastic IPs
674
+ const eipCommand = new DescribeAddressesCommand({});
675
+ const eipResponse = await this.ec2Client.send(eipCommand);
676
+
677
+ if (eipResponse.Addresses) {
678
+ for (const eip of eipResponse.Addresses) {
679
+ if (!eip.InstanceId && !eip.NetworkInterfaceId && !eip.AssociationId) {
680
+ // Check if it's Frigg-managed
681
+ const isFriggManaged = eip.Tags && eip.Tags.some(tag =>
682
+ tag.Key === 'ManagedBy' && tag.Value === 'Frigg'
683
+ );
684
+ if (isFriggManaged) {
685
+ misconfigurations.orphanedElasticIps.push({
686
+ allocationId: eip.AllocationId,
687
+ publicIp: eip.PublicIp,
688
+ tags: eip.Tags
689
+ });
690
+ }
691
+ }
692
+ }
693
+ }
694
+
695
+ // Find private subnets without NAT route
696
+ const subnets = await this.findPrivateSubnets(vpcId);
697
+ const routeTables = await this.findRouteTables(vpcId);
698
+
699
+ for (const subnet of subnets) {
700
+ let hasNatRoute = false;
701
+
702
+ // Find route table for this subnet
703
+ for (const rt of routeTables) {
704
+ const isAssociated = rt.Associations && rt.Associations.some(
705
+ assoc => assoc.SubnetId === subnet.SubnetId
706
+ );
707
+
708
+ if (isAssociated) {
709
+ hasNatRoute = rt.Routes && rt.Routes.some(
710
+ route => route.NatGatewayId &&
711
+ route.DestinationCidrBlock === '0.0.0.0/0'
712
+ );
713
+ break;
714
+ }
715
+ }
716
+
717
+ if (!hasNatRoute) {
718
+ misconfigurations.privateSubnetsWithoutNatRoute.push({
719
+ subnetId: subnet.SubnetId,
720
+ availabilityZone: subnet.AvailabilityZone
721
+ });
722
+ }
723
+ }
724
+
725
+ return misconfigurations;
726
+ } catch (error) {
727
+ console.error('Error detecting misconfigurations:', error);
728
+ return {
729
+ natGatewaysInPrivateSubnets: [],
730
+ orphanedElasticIps: [],
731
+ misconfiguredRouteTables: [],
732
+ privateSubnetsWithoutNatRoute: []
733
+ };
734
+ }
735
+ }
736
+
737
+ /**
738
+ * Get healing recommendations based on detected issues
739
+ * @param {Object} misconfigurations - Object from detectMisconfiguredResources
740
+ * @returns {Array} Array of healing recommendations
741
+ */
742
+ getHealingRecommendations(misconfigurations) {
743
+ const recommendations = [];
744
+
745
+ if (misconfigurations.natGatewaysInPrivateSubnets.length > 0) {
746
+ recommendations.push({
747
+ severity: 'critical',
748
+ issue: 'NAT Gateway in private subnet',
749
+ recommendation: 'Recreate NAT Gateway in public subnet or fix route tables',
750
+ affectedResources: misconfigurations.natGatewaysInPrivateSubnets.map(n => n.natGatewayId)
751
+ });
752
+ }
753
+
754
+ if (misconfigurations.orphanedElasticIps.length > 0) {
755
+ recommendations.push({
756
+ severity: 'warning',
757
+ issue: 'Orphaned Elastic IPs',
758
+ recommendation: 'Release unused Elastic IPs to avoid charges',
759
+ affectedResources: misconfigurations.orphanedElasticIps.map(e => e.allocationId)
760
+ });
761
+ }
762
+
763
+ if (misconfigurations.privateSubnetsWithoutNatRoute.length > 0) {
764
+ recommendations.push({
765
+ severity: 'critical',
766
+ issue: 'Private subnets without NAT route',
767
+ recommendation: 'Add NAT Gateway route to private subnet route tables',
768
+ affectedResources: misconfigurations.privateSubnetsWithoutNatRoute.map(s => s.subnetId)
769
+ });
770
+ }
771
+
772
+ // Sort by severity
773
+ recommendations.sort((a, b) => {
774
+ const severityOrder = { critical: 0, warning: 1, info: 2 };
775
+ return severityOrder[a.severity] - severityOrder[b.severity];
776
+ });
777
+
778
+ return recommendations;
779
+ }
780
+
517
781
  /**
518
782
  * Discover all AWS resources needed for Frigg deployment
519
783
  * @returns {Promise<Object>} Object containing discovered resource IDs:
@@ -621,6 +885,126 @@ class AWSDiscovery {
621
885
  return null;
622
886
  }
623
887
  }
888
+
889
+ /**
890
+ * Find Frigg-managed resources by tags
891
+ * @param {string} serviceName - The service name to search for
892
+ * @param {string} stage - The deployment stage
893
+ * @returns {Promise<Object>} Object containing found Frigg-managed resources
894
+ */
895
+ async findFriggManagedResources(serviceName, stage) {
896
+ try {
897
+ const results = {
898
+ natGateways: [],
899
+ elasticIps: [],
900
+ routeTables: [],
901
+ subnets: [],
902
+ securityGroups: []
903
+ };
904
+
905
+ // Common filter for Frigg-managed resources
906
+ const friggFilters = [
907
+ {
908
+ Name: 'tag:ManagedBy',
909
+ Values: ['Frigg']
910
+ }
911
+ ];
912
+
913
+ if (serviceName) {
914
+ friggFilters.push({
915
+ Name: 'tag:Service',
916
+ Values: [serviceName]
917
+ });
918
+ }
919
+
920
+ if (stage) {
921
+ friggFilters.push({
922
+ Name: 'tag:Stage',
923
+ Values: [stage]
924
+ });
925
+ }
926
+
927
+ // Find NAT Gateways
928
+ try {
929
+ const natCommand = new DescribeNatGatewaysCommand({
930
+ Filter: [
931
+ ...friggFilters,
932
+ {
933
+ Name: 'state',
934
+ Values: ['available']
935
+ }
936
+ ]
937
+ });
938
+ const natResponse = await this.ec2Client.send(natCommand);
939
+ results.natGateways = natResponse.NatGateways || [];
940
+ } catch (err) {
941
+ console.warn('Error finding Frigg NAT Gateways:', err.message);
942
+ }
943
+
944
+ // Find Elastic IPs
945
+ try {
946
+ const eipCommand = new DescribeAddressesCommand({
947
+ Filters: friggFilters
948
+ });
949
+ const eipResponse = await this.ec2Client.send(eipCommand);
950
+ results.elasticIps = eipResponse.Addresses || [];
951
+ } catch (err) {
952
+ console.warn('Error finding Frigg Elastic IPs:', err.message);
953
+ }
954
+
955
+ // Find Route Tables
956
+ try {
957
+ const rtCommand = new DescribeRouteTablesCommand({
958
+ Filters: friggFilters
959
+ });
960
+ const rtResponse = await this.ec2Client.send(rtCommand);
961
+ results.routeTables = rtResponse.RouteTables || [];
962
+ } catch (err) {
963
+ console.warn('Error finding Frigg Route Tables:', err.message);
964
+ }
965
+
966
+ // Find Subnets
967
+ try {
968
+ const subnetCommand = new DescribeSubnetsCommand({
969
+ Filters: friggFilters
970
+ });
971
+ const subnetResponse = await this.ec2Client.send(subnetCommand);
972
+ results.subnets = subnetResponse.Subnets || [];
973
+ } catch (err) {
974
+ console.warn('Error finding Frigg Subnets:', err.message);
975
+ }
976
+
977
+ // Find Security Groups
978
+ try {
979
+ const sgCommand = new DescribeSecurityGroupsCommand({
980
+ Filters: friggFilters
981
+ });
982
+ const sgResponse = await this.ec2Client.send(sgCommand);
983
+ results.securityGroups = sgResponse.SecurityGroups || [];
984
+ } catch (err) {
985
+ console.warn('Error finding Frigg Security Groups:', err.message);
986
+ }
987
+
988
+ console.log('Found Frigg-managed resources:', {
989
+ natGateways: results.natGateways.length,
990
+ elasticIps: results.elasticIps.length,
991
+ routeTables: results.routeTables.length,
992
+ subnets: results.subnets.length,
993
+ securityGroups: results.securityGroups.length
994
+ });
995
+
996
+ return results;
997
+ } catch (error) {
998
+ console.error('Error finding Frigg-managed resources:', error);
999
+ return {
1000
+ natGateways: [],
1001
+ elasticIps: [],
1002
+ routeTables: [],
1003
+ subnets: [],
1004
+ securityGroups: []
1005
+ };
1006
+ }
1007
+ }
624
1008
  }
625
1009
 
626
1010
  module.exports = { AWSDiscovery };