@friggframework/devtools 2.0.0--canary.428.887095f.0 โ 2.0.0--canary.428.6b04c24.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.
|
@@ -96,10 +96,11 @@ class AWSDiscovery {
|
|
|
96
96
|
/**
|
|
97
97
|
* Find private subnets for the given VPC
|
|
98
98
|
* @param {string} vpcId - The VPC ID to search within
|
|
99
|
+
* @param {boolean} autoConvert - If true, convert public subnets to private if needed
|
|
99
100
|
* @returns {Promise<Array>} Array of subnet objects (at least 2 for high availability)
|
|
100
101
|
* @throws {Error} If no subnets are found in the VPC
|
|
101
102
|
*/
|
|
102
|
-
async findPrivateSubnets(vpcId) {
|
|
103
|
+
async findPrivateSubnets(vpcId, autoConvert = false) {
|
|
103
104
|
try {
|
|
104
105
|
const command = new DescribeSubnetsCommand({
|
|
105
106
|
Filters: [
|
|
@@ -109,14 +110,16 @@ class AWSDiscovery {
|
|
|
109
110
|
}
|
|
110
111
|
]
|
|
111
112
|
});
|
|
112
|
-
|
|
113
|
+
|
|
113
114
|
const response = await this.ec2Client.send(command);
|
|
114
|
-
|
|
115
|
+
|
|
115
116
|
if (!response.Subnets || response.Subnets.length === 0) {
|
|
116
117
|
throw new Error(`No subnets found in VPC ${vpcId}`);
|
|
117
118
|
}
|
|
118
119
|
|
|
119
|
-
|
|
120
|
+
console.log(`\n๐ Analyzing ${response.Subnets.length} subnets in VPC ${vpcId}...`);
|
|
121
|
+
|
|
122
|
+
// Categorize subnets by their actual routing
|
|
120
123
|
const privateSubnets = [];
|
|
121
124
|
const publicSubnets = [];
|
|
122
125
|
|
|
@@ -125,17 +128,74 @@ class AWSDiscovery {
|
|
|
125
128
|
const isPrivate = await this.isSubnetPrivate(subnet.SubnetId);
|
|
126
129
|
if (isPrivate) {
|
|
127
130
|
privateSubnets.push(subnet);
|
|
131
|
+
console.log(` ๐ Private subnet: ${subnet.SubnetId} (AZ: ${subnet.AvailabilityZone})`);
|
|
128
132
|
} else {
|
|
129
133
|
publicSubnets.push(subnet);
|
|
134
|
+
console.log(` ๐ Public subnet: ${subnet.SubnetId} (AZ: ${subnet.AvailabilityZone})`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
console.log(`\n๐ Subnet Analysis Results:`);
|
|
139
|
+
console.log(` - Private subnets: ${privateSubnets.length}`);
|
|
140
|
+
console.log(` - Public subnets: ${publicSubnets.length}`);
|
|
141
|
+
|
|
142
|
+
// If we have at least 2 private subnets, use them
|
|
143
|
+
if (privateSubnets.length >= 2) {
|
|
144
|
+
console.log(`โ
Found ${privateSubnets.length} private subnets for Lambda deployment`);
|
|
145
|
+
return privateSubnets.slice(0, 2);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// If we have 1 private subnet, we need at least one more
|
|
149
|
+
if (privateSubnets.length === 1) {
|
|
150
|
+
console.warn(`โ ๏ธ Only 1 private subnet found. Need at least 2 for high availability.`);
|
|
151
|
+
if (publicSubnets.length > 0 && autoConvert) {
|
|
152
|
+
console.log(`๐ Will convert 1 public subnet to private for high availability...`);
|
|
153
|
+
// Note: The actual conversion happens in the serverless template
|
|
130
154
|
}
|
|
155
|
+
// Return what we have - mix of private and public if needed
|
|
156
|
+
return [...privateSubnets, ...publicSubnets].slice(0, 2);
|
|
131
157
|
}
|
|
132
158
|
|
|
133
|
-
//
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
159
|
+
// No private subnets found at all - this is a problem!
|
|
160
|
+
if (privateSubnets.length === 0 && publicSubnets.length > 0) {
|
|
161
|
+
console.error(`โ CRITICAL: No private subnets found, but ${publicSubnets.length} public subnets exist`);
|
|
162
|
+
console.error(`โ Lambda functions should NOT be deployed in public subnets!`);
|
|
163
|
+
|
|
164
|
+
if (autoConvert && publicSubnets.length >= 3) {
|
|
165
|
+
console.log(`\n๐ง AUTO-CONVERSION: Will configure subnets for proper isolation...`);
|
|
166
|
+
console.log(` - Keeping ${publicSubnets[0].SubnetId} as public (for NAT Gateway)`);
|
|
167
|
+
console.log(` - Converting ${publicSubnets[1].SubnetId} to private (for Lambda)`);
|
|
168
|
+
if (publicSubnets[2]) {
|
|
169
|
+
console.log(` - Converting ${publicSubnets[2].SubnetId} to private (for Lambda)`);
|
|
170
|
+
}
|
|
137
171
|
|
|
138
|
-
|
|
172
|
+
// Return subnets that SHOULD be private (indexes 1 and 2)
|
|
173
|
+
// The actual conversion happens in the serverless template
|
|
174
|
+
return publicSubnets.slice(1, 3);
|
|
175
|
+
} else if (autoConvert && publicSubnets.length >= 2) {
|
|
176
|
+
console.log(`\n๐ง AUTO-CONVERSION: Only ${publicSubnets.length} subnets available`);
|
|
177
|
+
console.log(` - Will need to create new subnets or reconfigure existing ones`);
|
|
178
|
+
// Return what we have but flag for conversion
|
|
179
|
+
return publicSubnets.slice(0, 2);
|
|
180
|
+
} else {
|
|
181
|
+
console.error(`\nโ ๏ธ CONFIGURATION ERROR:`);
|
|
182
|
+
console.error(` Found ${publicSubnets.length} public subnets but no private subnets.`);
|
|
183
|
+
console.error(` Lambda functions require private subnets for security.`);
|
|
184
|
+
console.error(`\n Options:`);
|
|
185
|
+
console.error(` 1. Enable selfHeal: true in vpc configuration`);
|
|
186
|
+
console.error(` 2. Create private subnets manually`);
|
|
187
|
+
console.error(` 3. Set subnets.management: 'create' to create new private subnets`);
|
|
188
|
+
|
|
189
|
+
throw new Error(
|
|
190
|
+
`No private subnets found in VPC ${vpcId}. ` +
|
|
191
|
+
`Found ${publicSubnets.length} public subnets. ` +
|
|
192
|
+
`Lambda requires private subnets. Enable selfHeal or create private subnets.`
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// No subnets at all?
|
|
198
|
+
throw new Error(`No subnets found in VPC ${vpcId}`);
|
|
139
199
|
} catch (error) {
|
|
140
200
|
console.error('Error finding private subnets:', error);
|
|
141
201
|
throw error;
|
|
@@ -450,6 +510,8 @@ class AWSDiscovery {
|
|
|
450
510
|
console.warn('This is a Frigg-managed NAT Gateway that may have been misconfigured by route table changes');
|
|
451
511
|
console.warn('Consider enabling selfHeal: true to fix this automatically');
|
|
452
512
|
// Return it anyway if it's Frigg-managed - we can fix the routes
|
|
513
|
+
// Mark that it's in a private subnet
|
|
514
|
+
natGateway._isInPrivateSubnet = true;
|
|
453
515
|
return natGateway;
|
|
454
516
|
} else {
|
|
455
517
|
console.warn('NAT Gateways MUST be placed in public subnets with Internet Gateway routes');
|
|
@@ -460,11 +522,13 @@ class AWSDiscovery {
|
|
|
460
522
|
|
|
461
523
|
if (isFriggNat) {
|
|
462
524
|
console.log(`Found existing Frigg-managed NAT Gateway: ${natGateway.NatGatewayId}`);
|
|
525
|
+
natGateway._isInPrivateSubnet = false;
|
|
463
526
|
return natGateway;
|
|
464
527
|
}
|
|
465
528
|
|
|
466
529
|
// Return first valid NAT Gateway that's in a public subnet
|
|
467
530
|
console.log(`Found existing NAT Gateway in public subnet: ${natGateway.NatGatewayId}`);
|
|
531
|
+
natGateway._isInPrivateSubnet = false;
|
|
468
532
|
return natGateway;
|
|
469
533
|
}
|
|
470
534
|
|
|
@@ -790,39 +854,51 @@ class AWSDiscovery {
|
|
|
790
854
|
* @returns {string|null} return.defaultKmsKeyId - Default KMS key ARN or null if not found
|
|
791
855
|
* @throws {Error} If resource discovery fails
|
|
792
856
|
*/
|
|
793
|
-
async discoverResources() {
|
|
857
|
+
async discoverResources(options = {}) {
|
|
794
858
|
try {
|
|
795
|
-
console.log('Discovering AWS resources for Frigg deployment...');
|
|
796
|
-
|
|
859
|
+
console.log('\n๐ Discovering AWS resources for Frigg deployment...');
|
|
860
|
+
console.log('โ'.repeat(60));
|
|
861
|
+
|
|
797
862
|
const vpc = await this.findDefaultVpc();
|
|
798
|
-
console.log(
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
863
|
+
console.log(`\nโ
Found VPC: ${vpc.VpcId}`);
|
|
864
|
+
|
|
865
|
+
// Enable auto-convert if selfHeal is enabled
|
|
866
|
+
const autoConvert = options.selfHeal || false;
|
|
867
|
+
|
|
868
|
+
const privateSubnets = await this.findPrivateSubnets(vpc.VpcId, autoConvert);
|
|
869
|
+
console.log(`\nโ
Selected subnets for Lambda: ${privateSubnets.map(s => s.SubnetId).join(', ')}`);
|
|
870
|
+
|
|
803
871
|
const publicSubnet = await this.findPublicSubnets(vpc.VpcId);
|
|
804
|
-
|
|
805
|
-
|
|
872
|
+
if (publicSubnet) {
|
|
873
|
+
console.log(`\nโ
Found public subnet for NAT Gateway: ${publicSubnet.SubnetId}`);
|
|
874
|
+
} else {
|
|
875
|
+
console.log(`\nโ ๏ธ No public subnet found - NAT Gateway creation may fail`);
|
|
876
|
+
}
|
|
877
|
+
|
|
806
878
|
const securityGroup = await this.findDefaultSecurityGroup(vpc.VpcId);
|
|
807
|
-
console.log(
|
|
808
|
-
|
|
879
|
+
console.log(`\nโ
Found security group: ${securityGroup.GroupId}`);
|
|
880
|
+
|
|
809
881
|
const routeTable = await this.findPrivateRouteTable(vpc.VpcId);
|
|
810
|
-
console.log(
|
|
811
|
-
|
|
882
|
+
console.log(`โ
Found route table: ${routeTable.RouteTableId}`);
|
|
883
|
+
|
|
812
884
|
const kmsKeyArn = await this.findDefaultKmsKey();
|
|
813
885
|
if (kmsKeyArn) {
|
|
814
|
-
console.log(
|
|
886
|
+
console.log(`โ
Found KMS key: ${kmsKeyArn}`);
|
|
815
887
|
} else {
|
|
816
|
-
console.log('No KMS key found');
|
|
888
|
+
console.log('โน๏ธ No KMS key found');
|
|
817
889
|
}
|
|
818
890
|
|
|
819
891
|
// Try to find existing NAT Gateway
|
|
820
892
|
const existingNatGateway = await this.findExistingNatGateway(vpc.VpcId);
|
|
821
893
|
let natGatewayId = null;
|
|
822
894
|
let elasticIpAllocationId = null;
|
|
823
|
-
|
|
895
|
+
let natGatewayInPrivateSubnet = false;
|
|
896
|
+
|
|
824
897
|
if (existingNatGateway) {
|
|
825
898
|
natGatewayId = existingNatGateway.NatGatewayId;
|
|
899
|
+
// Check if NAT Gateway is in a private subnet (from our detection)
|
|
900
|
+
natGatewayInPrivateSubnet = existingNatGateway._isInPrivateSubnet || false;
|
|
901
|
+
|
|
826
902
|
// Get the EIP allocation ID from the NAT Gateway
|
|
827
903
|
if (existingNatGateway.NatGatewayAddresses && existingNatGateway.NatGatewayAddresses.length > 0) {
|
|
828
904
|
elasticIpAllocationId = existingNatGateway.NatGatewayAddresses[0].AllocationId;
|
|
@@ -835,6 +911,42 @@ class AWSDiscovery {
|
|
|
835
911
|
}
|
|
836
912
|
}
|
|
837
913
|
|
|
914
|
+
// Check if the "private" subnets are actually public
|
|
915
|
+
const subnet1IsActuallyPrivate = privateSubnets[0] ?
|
|
916
|
+
await this.isSubnetPrivate(privateSubnets[0].SubnetId) : false;
|
|
917
|
+
const subnet2IsActuallyPrivate = privateSubnets[1] ?
|
|
918
|
+
await this.isSubnetPrivate(privateSubnets[1].SubnetId) :
|
|
919
|
+
subnet1IsActuallyPrivate;
|
|
920
|
+
|
|
921
|
+
const subnetStatus = {
|
|
922
|
+
requiresConversion: !subnet1IsActuallyPrivate || !subnet2IsActuallyPrivate,
|
|
923
|
+
subnet1NeedsConversion: !subnet1IsActuallyPrivate,
|
|
924
|
+
subnet2NeedsConversion: !subnet2IsActuallyPrivate
|
|
925
|
+
};
|
|
926
|
+
|
|
927
|
+
if (subnetStatus.requiresConversion) {
|
|
928
|
+
console.log(`\nโ ๏ธ SUBNET CONFIGURATION WARNING:`);
|
|
929
|
+
if (subnetStatus.subnet1NeedsConversion && privateSubnets[0]) {
|
|
930
|
+
console.log(` - Subnet ${privateSubnets[0].SubnetId} is currently PUBLIC but will be used for Lambda`);
|
|
931
|
+
}
|
|
932
|
+
if (subnetStatus.subnet2NeedsConversion && privateSubnets[1]) {
|
|
933
|
+
console.log(` - Subnet ${privateSubnets[1].SubnetId} is currently PUBLIC but will be used for Lambda`);
|
|
934
|
+
}
|
|
935
|
+
console.log(` ๐ก Enable selfHeal: true to automatically fix this`);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
console.log(`\n${'โ'.repeat(60)}`);
|
|
939
|
+
console.log('๐ Discovery Summary:');
|
|
940
|
+
console.log(` VPC: ${vpc.VpcId}`);
|
|
941
|
+
console.log(` Lambda Subnets: ${privateSubnets.map(s => s.SubnetId).join(', ')}`);
|
|
942
|
+
console.log(` NAT Subnet: ${publicSubnet?.SubnetId || 'None (needs creation)'}`);
|
|
943
|
+
console.log(` NAT Gateway: ${natGatewayId || 'None (will be created)'}`);
|
|
944
|
+
console.log(` Elastic IP: ${elasticIpAllocationId || 'None (will be allocated)'}`);
|
|
945
|
+
if (subnetStatus.requiresConversion) {
|
|
946
|
+
console.log(` โ ๏ธ Subnet Conversion Required: Yes`);
|
|
947
|
+
}
|
|
948
|
+
console.log(`${'โ'.repeat(60)}\n`);
|
|
949
|
+
|
|
838
950
|
return {
|
|
839
951
|
defaultVpcId: vpc.VpcId,
|
|
840
952
|
defaultSecurityGroupId: securityGroup.GroupId,
|
|
@@ -844,7 +956,19 @@ class AWSDiscovery {
|
|
|
844
956
|
privateRouteTableId: routeTable.RouteTableId,
|
|
845
957
|
defaultKmsKeyId: kmsKeyArn,
|
|
846
958
|
existingNatGatewayId: natGatewayId,
|
|
847
|
-
existingElasticIpAllocationId: elasticIpAllocationId
|
|
959
|
+
existingElasticIpAllocationId: elasticIpAllocationId,
|
|
960
|
+
natGatewayInPrivateSubnet: natGatewayInPrivateSubnet,
|
|
961
|
+
subnetConversionRequired: subnetStatus.requiresConversion,
|
|
962
|
+
privateSubnetsWithWrongRoutes: (() => {
|
|
963
|
+
const wrongRoutes = [];
|
|
964
|
+
if (subnetStatus.subnet1NeedsConversion && privateSubnets[0]) {
|
|
965
|
+
wrongRoutes.push(privateSubnets[0].SubnetId);
|
|
966
|
+
}
|
|
967
|
+
if (subnetStatus.subnet2NeedsConversion && privateSubnets[1]) {
|
|
968
|
+
wrongRoutes.push(privateSubnets[1].SubnetId);
|
|
969
|
+
}
|
|
970
|
+
return wrongRoutes;
|
|
971
|
+
})()
|
|
848
972
|
};
|
|
849
973
|
} catch (error) {
|
|
850
974
|
console.error('Error discovering AWS resources:', error);
|
|
@@ -1,298 +1,545 @@
|
|
|
1
|
-
const
|
|
1
|
+
const { mockClient } = require('aws-sdk-client-mock');
|
|
2
2
|
const { AWSDiscovery } = require('./aws-discovery');
|
|
3
3
|
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
4
|
+
// Import AWS SDK commands
|
|
5
|
+
const {
|
|
6
|
+
EC2Client,
|
|
7
|
+
DescribeVpcsCommand,
|
|
8
|
+
DescribeSubnetsCommand,
|
|
9
|
+
DescribeSecurityGroupsCommand,
|
|
10
|
+
DescribeRouteTablesCommand,
|
|
11
|
+
DescribeNatGatewaysCommand,
|
|
12
|
+
DescribeAddressesCommand
|
|
13
|
+
} = require('@aws-sdk/client-ec2');
|
|
14
|
+
const {
|
|
15
|
+
KMSClient,
|
|
16
|
+
ListKeysCommand,
|
|
17
|
+
DescribeKeyCommand
|
|
18
|
+
} = require('@aws-sdk/client-kms');
|
|
19
|
+
const {
|
|
20
|
+
STSClient,
|
|
21
|
+
GetCallerIdentityCommand
|
|
22
|
+
} = require('@aws-sdk/client-sts');
|
|
23
|
+
|
|
24
|
+
// Create mock clients
|
|
25
|
+
const ec2Mock = mockClient(EC2Client);
|
|
26
|
+
const kmsMock = mockClient(KMSClient);
|
|
27
|
+
const stsMock = mockClient(STSClient);
|
|
12
28
|
|
|
13
29
|
describe('AWSDiscovery', () => {
|
|
14
30
|
let discovery;
|
|
15
|
-
let mockEC2Send;
|
|
16
|
-
let mockKMSSend;
|
|
17
|
-
let mockSTSSend;
|
|
18
31
|
|
|
19
32
|
beforeEach(() => {
|
|
33
|
+
// Reset all mocks before each test
|
|
34
|
+
ec2Mock.reset();
|
|
35
|
+
kmsMock.reset();
|
|
36
|
+
stsMock.reset();
|
|
37
|
+
|
|
20
38
|
discovery = new AWSDiscovery('us-east-1');
|
|
21
|
-
|
|
22
|
-
// Create mock send functions
|
|
23
|
-
mockEC2Send = jest.fn();
|
|
24
|
-
mockKMSSend = jest.fn();
|
|
25
|
-
mockSTSSend = jest.fn();
|
|
26
|
-
|
|
27
|
-
// Mock the client constructors and send methods
|
|
28
|
-
EC2Client.mockImplementation(() => ({
|
|
29
|
-
send: mockEC2Send
|
|
30
|
-
}));
|
|
31
|
-
|
|
32
|
-
KMSClient.mockImplementation(() => ({
|
|
33
|
-
send: mockKMSSend
|
|
34
|
-
}));
|
|
35
|
-
|
|
36
|
-
STSClient.mockImplementation(() => ({
|
|
37
|
-
send: mockSTSSend
|
|
38
|
-
}));
|
|
39
|
-
|
|
40
|
-
// Reset mocks
|
|
41
|
-
jest.clearAllMocks();
|
|
42
39
|
});
|
|
43
40
|
|
|
44
41
|
describe('getAccountId', () => {
|
|
45
42
|
it('should return AWS account ID', async () => {
|
|
46
43
|
const mockAccountId = '123456789012';
|
|
47
|
-
|
|
44
|
+
stsMock.on(GetCallerIdentityCommand).resolves({
|
|
48
45
|
Account: mockAccountId
|
|
49
46
|
});
|
|
50
47
|
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
expect(result).toBe(mockAccountId);
|
|
54
|
-
expect(mockSTSSend).toHaveBeenCalledWith(expect.any(GetCallerIdentityCommand));
|
|
48
|
+
const accountId = await discovery.getAccountId();
|
|
49
|
+
expect(accountId).toBe(mockAccountId);
|
|
55
50
|
});
|
|
56
51
|
|
|
57
52
|
it('should throw error when STS call fails', async () => {
|
|
58
|
-
|
|
59
|
-
mockSTSSend.mockRejectedValue(error);
|
|
53
|
+
stsMock.on(GetCallerIdentityCommand).rejects(new Error('STS error'));
|
|
60
54
|
|
|
61
|
-
await expect(discovery.getAccountId()).rejects.toThrow('STS
|
|
55
|
+
await expect(discovery.getAccountId()).rejects.toThrow('STS error');
|
|
62
56
|
});
|
|
63
57
|
});
|
|
64
58
|
|
|
65
59
|
describe('findDefaultVpc', () => {
|
|
66
|
-
it('should return default VPC when
|
|
67
|
-
const mockVpc = {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
};
|
|
60
|
+
it('should return default VPC when available', async () => {
|
|
61
|
+
const mockVpc = { VpcId: 'vpc-12345678', IsDefault: true };
|
|
62
|
+
ec2Mock.on(DescribeVpcsCommand).resolves({
|
|
63
|
+
Vpcs: [mockVpc]
|
|
64
|
+
});
|
|
72
65
|
|
|
73
|
-
|
|
66
|
+
const vpc = await discovery.findDefaultVpc();
|
|
67
|
+
expect(vpc).toEqual(mockVpc);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should return first VPC when no default VPC exists', async () => {
|
|
71
|
+
const mockVpc = { VpcId: 'vpc-12345678', IsDefault: false };
|
|
72
|
+
ec2Mock.on(DescribeVpcsCommand).resolves({
|
|
74
73
|
Vpcs: [mockVpc]
|
|
75
74
|
});
|
|
76
75
|
|
|
77
|
-
const
|
|
76
|
+
const vpc = await discovery.findDefaultVpc();
|
|
77
|
+
expect(vpc).toEqual(mockVpc);
|
|
78
|
+
});
|
|
78
79
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}]
|
|
86
|
-
}
|
|
87
|
-
}));
|
|
80
|
+
it('should throw error when no VPCs found', async () => {
|
|
81
|
+
ec2Mock.on(DescribeVpcsCommand).resolves({
|
|
82
|
+
Vpcs: []
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
await expect(discovery.findDefaultVpc()).rejects.toThrow('No VPC found in the account');
|
|
88
86
|
});
|
|
87
|
+
});
|
|
89
88
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
IsDefault: false,
|
|
94
|
-
State: 'available'
|
|
95
|
-
};
|
|
89
|
+
describe('isSubnetPrivate', () => {
|
|
90
|
+
const mockVpcId = 'vpc-12345678';
|
|
91
|
+
const mockSubnetId = 'subnet-12345678';
|
|
96
92
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
93
|
+
it('should return true for private subnet', async () => {
|
|
94
|
+
// Mock subnet lookup first
|
|
95
|
+
ec2Mock.on(DescribeSubnetsCommand).resolves({
|
|
96
|
+
Subnets: [{ SubnetId: mockSubnetId, VpcId: mockVpcId }]
|
|
97
|
+
});
|
|
100
98
|
|
|
101
|
-
|
|
99
|
+
ec2Mock.on(DescribeRouteTablesCommand).resolves({
|
|
100
|
+
RouteTables: [{
|
|
101
|
+
Associations: [{ SubnetId: mockSubnetId }],
|
|
102
|
+
Routes: [
|
|
103
|
+
{ GatewayId: 'local', DestinationCidrBlock: '10.0.0.0/16' }
|
|
104
|
+
]
|
|
105
|
+
}]
|
|
106
|
+
});
|
|
102
107
|
|
|
103
|
-
|
|
104
|
-
expect(
|
|
108
|
+
const isPrivate = await discovery.isSubnetPrivate(mockSubnetId, mockVpcId);
|
|
109
|
+
expect(isPrivate).toBe(true);
|
|
105
110
|
});
|
|
106
111
|
|
|
107
|
-
it('should
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
112
|
+
it('should return false for public subnet', async () => {
|
|
113
|
+
// Mock subnet lookup first
|
|
114
|
+
ec2Mock.on(DescribeSubnetsCommand).resolves({
|
|
115
|
+
Subnets: [{ SubnetId: mockSubnetId, VpcId: mockVpcId }]
|
|
116
|
+
});
|
|
111
117
|
|
|
112
|
-
|
|
118
|
+
ec2Mock.on(DescribeRouteTablesCommand).resolves({
|
|
119
|
+
RouteTables: [{
|
|
120
|
+
Associations: [{ SubnetId: mockSubnetId }],
|
|
121
|
+
Routes: [
|
|
122
|
+
{ GatewayId: 'igw-12345', DestinationCidrBlock: '0.0.0.0/0' }
|
|
123
|
+
]
|
|
124
|
+
}]
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const isPrivate = await discovery.isSubnetPrivate(mockSubnetId, mockVpcId);
|
|
128
|
+
expect(isPrivate).toBe(false);
|
|
113
129
|
});
|
|
114
130
|
});
|
|
115
131
|
|
|
116
132
|
describe('findPrivateSubnets', () => {
|
|
117
133
|
const mockVpcId = 'vpc-12345678';
|
|
118
134
|
|
|
119
|
-
it('should return private subnets
|
|
135
|
+
it('should return private subnets', async () => {
|
|
120
136
|
const mockSubnets = [
|
|
121
|
-
{ SubnetId: 'subnet-private-1',
|
|
122
|
-
{ SubnetId: 'subnet-private-2',
|
|
137
|
+
{ SubnetId: 'subnet-private-1', AvailabilityZone: 'us-east-1a' },
|
|
138
|
+
{ SubnetId: 'subnet-private-2', AvailabilityZone: 'us-east-1b' }
|
|
123
139
|
];
|
|
124
140
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
141
|
+
ec2Mock.on(DescribeSubnetsCommand).resolves({
|
|
142
|
+
Subnets: mockSubnets
|
|
143
|
+
});
|
|
128
144
|
|
|
129
|
-
|
|
145
|
+
// Mock route tables - no IGW routes (private)
|
|
146
|
+
ec2Mock.on(DescribeRouteTablesCommand).resolves({
|
|
147
|
+
RouteTables: [{
|
|
148
|
+
Associations: [
|
|
149
|
+
{ SubnetId: 'subnet-private-1' },
|
|
150
|
+
{ SubnetId: 'subnet-private-2' }
|
|
151
|
+
],
|
|
152
|
+
Routes: [
|
|
153
|
+
{ GatewayId: 'local', DestinationCidrBlock: '10.0.0.0/16' }
|
|
154
|
+
]
|
|
155
|
+
}]
|
|
156
|
+
});
|
|
130
157
|
|
|
131
|
-
|
|
132
|
-
expect(
|
|
133
|
-
expect(result[1].SubnetId).toBe('subnet-private-2');
|
|
158
|
+
const subnets = await discovery.findPrivateSubnets(mockVpcId);
|
|
159
|
+
expect(subnets).toEqual(mockSubnets);
|
|
134
160
|
});
|
|
135
161
|
|
|
136
|
-
it('should
|
|
162
|
+
it('should throw error when no private subnets found and autoConvert is false', async () => {
|
|
137
163
|
const mockSubnets = [
|
|
138
|
-
{ SubnetId: 'subnet-1',
|
|
139
|
-
{ SubnetId: 'subnet-2',
|
|
140
|
-
{ SubnetId: 'subnet-3',
|
|
164
|
+
{ SubnetId: 'subnet-public-1', AvailabilityZone: 'us-east-1a' },
|
|
165
|
+
{ SubnetId: 'subnet-public-2', AvailabilityZone: 'us-east-1b' },
|
|
166
|
+
{ SubnetId: 'subnet-public-3', AvailabilityZone: 'us-east-1c' }
|
|
141
167
|
];
|
|
142
168
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
RouteTables: [{
|
|
147
|
-
Routes: [] // No IGW route = private
|
|
148
|
-
}]
|
|
149
|
-
});
|
|
169
|
+
ec2Mock.on(DescribeSubnetsCommand).resolves({
|
|
170
|
+
Subnets: mockSubnets
|
|
171
|
+
});
|
|
150
172
|
|
|
151
|
-
|
|
173
|
+
// Mock route tables - has IGW routes (public)
|
|
174
|
+
ec2Mock.on(DescribeRouteTablesCommand).resolves({
|
|
175
|
+
RouteTables: [{
|
|
176
|
+
Associations: [
|
|
177
|
+
{ SubnetId: 'subnet-public-1' },
|
|
178
|
+
{ SubnetId: 'subnet-public-2' },
|
|
179
|
+
{ SubnetId: 'subnet-public-3' }
|
|
180
|
+
],
|
|
181
|
+
Routes: [
|
|
182
|
+
{ GatewayId: 'igw-12345', DestinationCidrBlock: '0.0.0.0/0' }
|
|
183
|
+
]
|
|
184
|
+
}]
|
|
185
|
+
});
|
|
152
186
|
|
|
153
|
-
expect(
|
|
187
|
+
await expect(discovery.findPrivateSubnets(mockVpcId, false))
|
|
188
|
+
.rejects.toThrow('No private subnets found in VPC');
|
|
154
189
|
});
|
|
155
190
|
|
|
156
|
-
it('should
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
});
|
|
191
|
+
it('should return public subnets with warning when autoConvert is true', async () => {
|
|
192
|
+
const mockSubnets = [
|
|
193
|
+
{ SubnetId: 'subnet-public-1', AvailabilityZone: 'us-east-1a' },
|
|
194
|
+
{ SubnetId: 'subnet-public-2', AvailabilityZone: 'us-east-1b' }
|
|
195
|
+
];
|
|
162
196
|
|
|
163
|
-
|
|
164
|
-
|
|
197
|
+
ec2Mock.on(DescribeSubnetsCommand).resolves({
|
|
198
|
+
Subnets: mockSubnets
|
|
199
|
+
});
|
|
165
200
|
|
|
166
|
-
|
|
167
|
-
|
|
201
|
+
// Mock route tables - has IGW routes (public)
|
|
202
|
+
ec2Mock.on(DescribeRouteTablesCommand).resolves({
|
|
168
203
|
RouteTables: [{
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
204
|
+
Associations: [
|
|
205
|
+
{ SubnetId: 'subnet-public-1' },
|
|
206
|
+
{ SubnetId: 'subnet-public-2' }
|
|
207
|
+
],
|
|
208
|
+
Routes: [
|
|
209
|
+
{ GatewayId: 'igw-12345', DestinationCidrBlock: '0.0.0.0/0' }
|
|
210
|
+
]
|
|
173
211
|
}]
|
|
174
212
|
});
|
|
175
213
|
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
expect(
|
|
214
|
+
const subnets = await discovery.findPrivateSubnets(mockVpcId, true);
|
|
215
|
+
expect(subnets).toHaveLength(2);
|
|
216
|
+
expect(subnets[0].SubnetId).toBe('subnet-public-1');
|
|
179
217
|
});
|
|
218
|
+
});
|
|
180
219
|
|
|
181
|
-
|
|
182
|
-
|
|
220
|
+
describe('findPublicSubnets', () => {
|
|
221
|
+
const mockVpcId = 'vpc-12345678';
|
|
222
|
+
|
|
223
|
+
it('should return public subnet', async () => {
|
|
224
|
+
const mockSubnet = { SubnetId: 'subnet-public-1' };
|
|
225
|
+
ec2Mock.on(DescribeSubnetsCommand).resolves({
|
|
226
|
+
Subnets: [mockSubnet]
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// Mock route table check to show it's public
|
|
230
|
+
ec2Mock.on(DescribeRouteTablesCommand).resolves({
|
|
183
231
|
RouteTables: [{
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
DestinationCidrBlock: '10.0.0.0/16'
|
|
187
|
-
}]
|
|
232
|
+
Associations: [{ SubnetId: 'subnet-public-1' }],
|
|
233
|
+
Routes: [{ GatewayId: 'igw-12345' }] // Has IGW = public
|
|
188
234
|
}]
|
|
189
235
|
});
|
|
190
236
|
|
|
191
|
-
const
|
|
237
|
+
const subnet = await discovery.findPublicSubnets(mockVpcId);
|
|
238
|
+
expect(subnet).toEqual(mockSubnet);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('should throw error when no subnets found', async () => {
|
|
242
|
+
ec2Mock.on(DescribeSubnetsCommand).resolves({
|
|
243
|
+
Subnets: []
|
|
244
|
+
});
|
|
192
245
|
|
|
193
|
-
expect(
|
|
246
|
+
await expect(discovery.findPublicSubnets(mockVpcId))
|
|
247
|
+
.rejects.toThrow('No subnets found in VPC');
|
|
194
248
|
});
|
|
249
|
+
});
|
|
195
250
|
|
|
196
|
-
|
|
197
|
-
|
|
251
|
+
describe('findDefaultSecurityGroup', () => {
|
|
252
|
+
const mockVpcId = 'vpc-12345678';
|
|
198
253
|
|
|
199
|
-
|
|
254
|
+
it('should return default security group', async () => {
|
|
255
|
+
const mockSecurityGroup = {
|
|
256
|
+
GroupId: 'sg-12345678',
|
|
257
|
+
GroupName: 'default'
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
ec2Mock.on(DescribeSecurityGroupsCommand).resolves({
|
|
261
|
+
SecurityGroups: [mockSecurityGroup]
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
const sg = await discovery.findDefaultSecurityGroup(mockVpcId);
|
|
265
|
+
expect(sg).toEqual(mockSecurityGroup);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('should throw error when no default security group found', async () => {
|
|
269
|
+
ec2Mock.on(DescribeSecurityGroupsCommand).resolves({
|
|
270
|
+
SecurityGroups: []
|
|
271
|
+
});
|
|
200
272
|
|
|
201
|
-
expect(
|
|
273
|
+
await expect(discovery.findDefaultSecurityGroup(mockVpcId))
|
|
274
|
+
.rejects.toThrow('No security group found for VPC');
|
|
202
275
|
});
|
|
203
276
|
});
|
|
204
277
|
|
|
205
|
-
describe('
|
|
278
|
+
describe('findPrivateRouteTable', () => {
|
|
206
279
|
const mockVpcId = 'vpc-12345678';
|
|
207
280
|
|
|
208
|
-
it('should return
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
281
|
+
it('should return private route table', async () => {
|
|
282
|
+
const mockRouteTable = {
|
|
283
|
+
RouteTableId: 'rtb-12345678',
|
|
284
|
+
Routes: [
|
|
285
|
+
{ GatewayId: 'local', DestinationCidrBlock: '10.0.0.0/16' }
|
|
286
|
+
]
|
|
213
287
|
};
|
|
214
288
|
|
|
215
|
-
|
|
216
|
-
|
|
289
|
+
ec2Mock.on(DescribeRouteTablesCommand).resolves({
|
|
290
|
+
RouteTables: [mockRouteTable]
|
|
217
291
|
});
|
|
218
292
|
|
|
219
|
-
const
|
|
293
|
+
const rt = await discovery.findPrivateRouteTable(mockVpcId);
|
|
294
|
+
expect(rt).toEqual(mockRouteTable);
|
|
295
|
+
});
|
|
220
296
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
297
|
+
it('should return first route table when no private route table found', async () => {
|
|
298
|
+
const mockRouteTable = {
|
|
299
|
+
RouteTableId: 'rtb-12345678',
|
|
300
|
+
Routes: [
|
|
301
|
+
{ GatewayId: 'igw-12345', DestinationCidrBlock: '0.0.0.0/0' }
|
|
302
|
+
]
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
ec2Mock.on(DescribeRouteTablesCommand).resolves({
|
|
306
|
+
RouteTables: [mockRouteTable]
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
const rt = await discovery.findPrivateRouteTable(mockVpcId);
|
|
310
|
+
expect(rt).toEqual(mockRouteTable);
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
describe('findDefaultKmsKey', () => {
|
|
315
|
+
it('should return default KMS key ARN', async () => {
|
|
316
|
+
const mockKeyId = '12345678-1234-1234-1234-123456789012';
|
|
317
|
+
const mockKeyArn = `arn:aws:kms:us-east-1:123456789012:key/${mockKeyId}`;
|
|
318
|
+
|
|
319
|
+
kmsMock.on(ListKeysCommand).resolves({
|
|
320
|
+
Keys: [{ KeyId: mockKeyId }]
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
kmsMock.on(DescribeKeyCommand).resolves({
|
|
324
|
+
KeyMetadata: {
|
|
325
|
+
Arn: mockKeyArn,
|
|
326
|
+
KeyManager: 'CUSTOMER',
|
|
327
|
+
KeyState: 'Enabled'
|
|
228
328
|
}
|
|
229
|
-
})
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
const keyArn = await discovery.findDefaultKmsKey();
|
|
332
|
+
expect(keyArn).toBe(mockKeyArn);
|
|
230
333
|
});
|
|
231
334
|
|
|
232
|
-
it('should
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
VpcId: mockVpcId
|
|
237
|
-
};
|
|
335
|
+
it('should return null when no AWS-managed keys found', async () => {
|
|
336
|
+
kmsMock.on(ListKeysCommand).resolves({
|
|
337
|
+
Keys: []
|
|
338
|
+
});
|
|
238
339
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
340
|
+
const keyArn = await discovery.findDefaultKmsKey();
|
|
341
|
+
expect(keyArn).toBeNull();
|
|
342
|
+
});
|
|
343
|
+
});
|
|
242
344
|
|
|
243
|
-
|
|
345
|
+
describe('findAvailableElasticIP', () => {
|
|
346
|
+
it('should return available Elastic IP', async () => {
|
|
347
|
+
const mockElasticIP = {
|
|
348
|
+
AllocationId: 'eipalloc-12345',
|
|
349
|
+
PublicIp: '52.1.2.3'
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
ec2Mock.on(DescribeAddressesCommand).resolves({
|
|
353
|
+
Addresses: [mockElasticIP]
|
|
354
|
+
});
|
|
244
355
|
|
|
245
|
-
|
|
246
|
-
expect(
|
|
356
|
+
const eip = await discovery.findAvailableElasticIP();
|
|
357
|
+
expect(eip).toEqual(mockElasticIP);
|
|
247
358
|
});
|
|
248
359
|
|
|
249
|
-
it('should
|
|
250
|
-
|
|
360
|
+
it('should return null when no available Elastic IPs', async () => {
|
|
361
|
+
ec2Mock.on(DescribeAddressesCommand).resolves({
|
|
362
|
+
Addresses: []
|
|
363
|
+
});
|
|
251
364
|
|
|
252
|
-
await
|
|
365
|
+
const eip = await discovery.findAvailableElasticIP();
|
|
366
|
+
expect(eip).toBeNull();
|
|
253
367
|
});
|
|
254
368
|
});
|
|
255
369
|
|
|
256
|
-
describe('
|
|
257
|
-
|
|
258
|
-
const mockKeyId = 'key-12345678';
|
|
259
|
-
const mockKeyArn = 'arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012';
|
|
370
|
+
describe('findExistingNatGateway', () => {
|
|
371
|
+
const mockVpcId = 'vpc-12345678';
|
|
260
372
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
373
|
+
beforeEach(() => {
|
|
374
|
+
// Create a fresh discovery instance for each test
|
|
375
|
+
discovery = new AWSDiscovery('us-east-1');
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it('should return NAT Gateway in public subnet', async () => {
|
|
379
|
+
const mockNatGateway = {
|
|
380
|
+
NatGatewayId: 'nat-12345678',
|
|
381
|
+
SubnetId: 'subnet-public-1',
|
|
382
|
+
State: 'available',
|
|
383
|
+
NatGatewayAddresses: [{ AllocationId: 'eipalloc-12345' }],
|
|
384
|
+
Tags: []
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
ec2Mock.on(DescribeNatGatewaysCommand).resolves({
|
|
388
|
+
NatGateways: [mockNatGateway]
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
// Mock subnet lookup
|
|
392
|
+
ec2Mock.on(DescribeSubnetsCommand).resolves({
|
|
393
|
+
Subnets: [{ SubnetId: 'subnet-public-1', VpcId: mockVpcId }]
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
// Mock route table - has IGW (public)
|
|
397
|
+
ec2Mock.on(DescribeRouteTablesCommand).resolves({
|
|
398
|
+
RouteTables: [{
|
|
399
|
+
Associations: [{ SubnetId: 'subnet-public-1' }],
|
|
400
|
+
Routes: [{ GatewayId: 'igw-12345' }]
|
|
401
|
+
}]
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
const result = await discovery.findExistingNatGateway(mockVpcId);
|
|
405
|
+
|
|
406
|
+
expect(result).toBeDefined();
|
|
407
|
+
expect(result.NatGatewayId).toBe('nat-12345678');
|
|
408
|
+
expect(result._isInPrivateSubnet).toBe(false);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it('should detect NAT Gateway in private subnet', async () => {
|
|
412
|
+
const mockNatGateway = {
|
|
413
|
+
NatGatewayId: 'nat-12345678',
|
|
414
|
+
SubnetId: 'subnet-private-1',
|
|
415
|
+
State: 'available',
|
|
416
|
+
NatGatewayAddresses: [{ AllocationId: 'eipalloc-12345' }],
|
|
417
|
+
Tags: [
|
|
418
|
+
{ Key: 'ManagedBy', Value: 'Frigg' }
|
|
419
|
+
]
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
ec2Mock.on(DescribeNatGatewaysCommand).resolves({
|
|
423
|
+
NatGateways: [mockNatGateway]
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
ec2Mock.on(DescribeSubnetsCommand).resolves({
|
|
427
|
+
Subnets: [{ SubnetId: 'subnet-private-1', VpcId: mockVpcId }]
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
// Mock route table - no IGW (private)
|
|
431
|
+
ec2Mock.on(DescribeRouteTablesCommand).resolves({
|
|
432
|
+
RouteTables: [{
|
|
433
|
+
Associations: [{ SubnetId: 'subnet-private-1' }],
|
|
434
|
+
Routes: [{ GatewayId: 'local' }]
|
|
435
|
+
}]
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
const result = await discovery.findExistingNatGateway(mockVpcId);
|
|
439
|
+
|
|
440
|
+
expect(result).toBeDefined();
|
|
441
|
+
expect(result.NatGatewayId).toBe('nat-12345678');
|
|
442
|
+
expect(result._isInPrivateSubnet).toBe(true);
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it('should skip non-Frigg NAT Gateway in private subnet', async () => {
|
|
446
|
+
const mockNatGateways = [
|
|
447
|
+
{
|
|
448
|
+
NatGatewayId: 'nat-other-12345',
|
|
449
|
+
SubnetId: 'subnet-private-1',
|
|
450
|
+
State: 'available',
|
|
451
|
+
Tags: [] // No Frigg tags
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
NatGatewayId: 'nat-good-12345',
|
|
455
|
+
SubnetId: 'subnet-public-1',
|
|
456
|
+
State: 'available',
|
|
457
|
+
Tags: []
|
|
458
|
+
}
|
|
459
|
+
];
|
|
460
|
+
|
|
461
|
+
ec2Mock.on(DescribeNatGatewaysCommand).resolves({
|
|
462
|
+
NatGateways: mockNatGateways
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
// First call for subnet-private-1
|
|
466
|
+
ec2Mock.on(DescribeSubnetsCommand)
|
|
467
|
+
.resolvesOnce({
|
|
468
|
+
Subnets: [{ SubnetId: 'subnet-private-1', VpcId: mockVpcId }]
|
|
264
469
|
})
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
Arn: mockKeyArn,
|
|
269
|
-
KeyManager: 'CUSTOMER',
|
|
270
|
-
KeyState: 'Enabled'
|
|
271
|
-
}
|
|
470
|
+
// Second call for subnet-public-1
|
|
471
|
+
.resolvesOnce({
|
|
472
|
+
Subnets: [{ SubnetId: 'subnet-public-1', VpcId: mockVpcId }]
|
|
272
473
|
});
|
|
273
474
|
|
|
274
|
-
|
|
475
|
+
// First call for private subnet route table
|
|
476
|
+
ec2Mock.on(DescribeRouteTablesCommand)
|
|
477
|
+
.resolvesOnce({
|
|
478
|
+
RouteTables: [{
|
|
479
|
+
Associations: [{ SubnetId: 'subnet-private-1' }],
|
|
480
|
+
Routes: [{ GatewayId: 'local' }] // Private
|
|
481
|
+
}]
|
|
482
|
+
})
|
|
483
|
+
// Second call for public subnet route table
|
|
484
|
+
.resolvesOnce({
|
|
485
|
+
RouteTables: [{
|
|
486
|
+
Associations: [{ SubnetId: 'subnet-public-1' }],
|
|
487
|
+
Routes: [{ GatewayId: 'igw-12345' }] // Public
|
|
488
|
+
}]
|
|
489
|
+
});
|
|
275
490
|
|
|
276
|
-
const result = await discovery.
|
|
491
|
+
const result = await discovery.findExistingNatGateway(mockVpcId);
|
|
277
492
|
|
|
278
|
-
expect(result).
|
|
493
|
+
expect(result).toBeDefined();
|
|
494
|
+
expect(result.NatGatewayId).toBe('nat-good-12345');
|
|
495
|
+
expect(result._isInPrivateSubnet).toBe(false);
|
|
279
496
|
});
|
|
280
497
|
|
|
281
|
-
it('should
|
|
282
|
-
|
|
283
|
-
|
|
498
|
+
it('should prioritize Frigg-managed NAT Gateways', async () => {
|
|
499
|
+
const mockNatGateways = [
|
|
500
|
+
{
|
|
501
|
+
NatGatewayId: 'nat-other-12345',
|
|
502
|
+
SubnetId: 'subnet-public-1',
|
|
503
|
+
State: 'available',
|
|
504
|
+
Tags: []
|
|
505
|
+
},
|
|
506
|
+
{
|
|
507
|
+
NatGatewayId: 'nat-frigg-12345',
|
|
508
|
+
SubnetId: 'subnet-public-2',
|
|
509
|
+
State: 'available',
|
|
510
|
+
Tags: [{ Key: 'ManagedBy', Value: 'Frigg' }]
|
|
511
|
+
}
|
|
512
|
+
];
|
|
284
513
|
|
|
285
|
-
|
|
514
|
+
ec2Mock.on(DescribeNatGatewaysCommand).resolves({
|
|
515
|
+
NatGateways: mockNatGateways
|
|
516
|
+
});
|
|
286
517
|
|
|
287
|
-
|
|
288
|
-
|
|
518
|
+
ec2Mock.on(DescribeSubnetsCommand).resolves({
|
|
519
|
+
Subnets: [{ SubnetId: 'subnet-public-2', VpcId: mockVpcId }]
|
|
520
|
+
});
|
|
289
521
|
|
|
290
|
-
|
|
291
|
-
|
|
522
|
+
ec2Mock.on(DescribeRouteTablesCommand).resolves({
|
|
523
|
+
RouteTables: [{
|
|
524
|
+
Associations: [{ SubnetId: 'subnet-public-2' }],
|
|
525
|
+
Routes: [{ GatewayId: 'igw-12345' }] // Public
|
|
526
|
+
}]
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
const result = await discovery.findExistingNatGateway(mockVpcId);
|
|
530
|
+
|
|
531
|
+
expect(result).toBeDefined();
|
|
532
|
+
expect(result.NatGatewayId).toBe('nat-frigg-12345');
|
|
533
|
+
expect(result._isInPrivateSubnet).toBe(false);
|
|
534
|
+
});
|
|
292
535
|
|
|
293
|
-
|
|
536
|
+
it('should return null when no NAT Gateways found', async () => {
|
|
537
|
+
ec2Mock.on(DescribeNatGatewaysCommand).resolves({
|
|
538
|
+
NatGateways: []
|
|
539
|
+
});
|
|
294
540
|
|
|
295
|
-
|
|
541
|
+
const result = await discovery.findExistingNatGateway(mockVpcId);
|
|
542
|
+
expect(result).toBeNull();
|
|
296
543
|
});
|
|
297
544
|
});
|
|
298
545
|
|
|
@@ -300,48 +547,171 @@ describe('AWSDiscovery', () => {
|
|
|
300
547
|
it('should discover all AWS resources successfully', async () => {
|
|
301
548
|
const mockVpc = { VpcId: 'vpc-12345678' };
|
|
302
549
|
const mockSubnets = [
|
|
303
|
-
{ SubnetId: 'subnet-1' },
|
|
550
|
+
{ SubnetId: 'subnet-1' },
|
|
304
551
|
{ SubnetId: 'subnet-2' }
|
|
305
552
|
];
|
|
553
|
+
const mockPublicSubnet = { SubnetId: 'subnet-public-1' };
|
|
306
554
|
const mockSecurityGroup = { GroupId: 'sg-12345678' };
|
|
307
555
|
const mockRouteTable = { RouteTableId: 'rtb-12345678' };
|
|
308
556
|
const mockKmsArn = 'arn:aws:kms:us-east-1:123456789012:key/12345678';
|
|
557
|
+
const mockNatGateway = {
|
|
558
|
+
NatGatewayId: 'nat-12345678',
|
|
559
|
+
SubnetId: 'subnet-public-1',
|
|
560
|
+
NatGatewayAddresses: [{ AllocationId: 'eipalloc-12345' }],
|
|
561
|
+
_isInPrivateSubnet: false
|
|
562
|
+
};
|
|
309
563
|
|
|
310
564
|
// Mock all the discovery methods
|
|
311
565
|
jest.spyOn(discovery, 'findDefaultVpc').mockResolvedValue(mockVpc);
|
|
312
566
|
jest.spyOn(discovery, 'findPrivateSubnets').mockResolvedValue(mockSubnets);
|
|
567
|
+
jest.spyOn(discovery, 'findPublicSubnets').mockResolvedValue(mockPublicSubnet);
|
|
313
568
|
jest.spyOn(discovery, 'findDefaultSecurityGroup').mockResolvedValue(mockSecurityGroup);
|
|
314
569
|
jest.spyOn(discovery, 'findPrivateRouteTable').mockResolvedValue(mockRouteTable);
|
|
315
570
|
jest.spyOn(discovery, 'findDefaultKmsKey').mockResolvedValue(mockKmsArn);
|
|
571
|
+
jest.spyOn(discovery, 'findExistingNatGateway').mockResolvedValue(mockNatGateway);
|
|
572
|
+
jest.spyOn(discovery, 'isSubnetPrivate')
|
|
573
|
+
.mockResolvedValueOnce(true) // subnet-1 is private
|
|
574
|
+
.mockResolvedValueOnce(true); // subnet-2 is private
|
|
316
575
|
|
|
317
576
|
const result = await discovery.discoverResources();
|
|
318
577
|
|
|
319
|
-
expect(result).
|
|
578
|
+
expect(result).toMatchObject({
|
|
320
579
|
defaultVpcId: 'vpc-12345678',
|
|
321
580
|
defaultSecurityGroupId: 'sg-12345678',
|
|
322
581
|
privateSubnetId1: 'subnet-1',
|
|
323
582
|
privateSubnetId2: 'subnet-2',
|
|
583
|
+
publicSubnetId: 'subnet-public-1',
|
|
324
584
|
privateRouteTableId: 'rtb-12345678',
|
|
325
|
-
defaultKmsKeyId: mockKmsArn
|
|
585
|
+
defaultKmsKeyId: mockKmsArn,
|
|
586
|
+
existingNatGatewayId: 'nat-12345678',
|
|
587
|
+
existingElasticIpAllocationId: 'eipalloc-12345',
|
|
588
|
+
natGatewayInPrivateSubnet: false,
|
|
589
|
+
subnetConversionRequired: false,
|
|
590
|
+
privateSubnetsWithWrongRoutes: []
|
|
326
591
|
});
|
|
327
592
|
|
|
328
593
|
// Verify all methods were called
|
|
329
594
|
expect(discovery.findDefaultVpc).toHaveBeenCalled();
|
|
330
|
-
expect(discovery.findPrivateSubnets).toHaveBeenCalledWith('vpc-12345678');
|
|
595
|
+
expect(discovery.findPrivateSubnets).toHaveBeenCalledWith('vpc-12345678', false);
|
|
596
|
+
expect(discovery.findPublicSubnets).toHaveBeenCalledWith('vpc-12345678');
|
|
331
597
|
expect(discovery.findDefaultSecurityGroup).toHaveBeenCalledWith('vpc-12345678');
|
|
332
598
|
expect(discovery.findPrivateRouteTable).toHaveBeenCalledWith('vpc-12345678');
|
|
333
599
|
expect(discovery.findDefaultKmsKey).toHaveBeenCalled();
|
|
600
|
+
expect(discovery.findExistingNatGateway).toHaveBeenCalledWith('vpc-12345678');
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
it('should detect subnet conversion requirements', async () => {
|
|
604
|
+
const mockVpc = { VpcId: 'vpc-12345678' };
|
|
605
|
+
const mockSubnets = [
|
|
606
|
+
{ SubnetId: 'subnet-1' },
|
|
607
|
+
{ SubnetId: 'subnet-2' }
|
|
608
|
+
];
|
|
609
|
+
const mockPublicSubnet = { SubnetId: 'subnet-public-1' };
|
|
610
|
+
const mockSecurityGroup = { GroupId: 'sg-12345678' };
|
|
611
|
+
const mockRouteTable = { RouteTableId: 'rtb-12345678' };
|
|
612
|
+
|
|
613
|
+
jest.spyOn(discovery, 'findDefaultVpc').mockResolvedValue(mockVpc);
|
|
614
|
+
jest.spyOn(discovery, 'findPrivateSubnets').mockResolvedValue(mockSubnets);
|
|
615
|
+
jest.spyOn(discovery, 'findPublicSubnets').mockResolvedValue(mockPublicSubnet);
|
|
616
|
+
jest.spyOn(discovery, 'findDefaultSecurityGroup').mockResolvedValue(mockSecurityGroup);
|
|
617
|
+
jest.spyOn(discovery, 'findPrivateRouteTable').mockResolvedValue(mockRouteTable);
|
|
618
|
+
jest.spyOn(discovery, 'findDefaultKmsKey').mockResolvedValue(null);
|
|
619
|
+
jest.spyOn(discovery, 'findExistingNatGateway').mockResolvedValue(null);
|
|
620
|
+
jest.spyOn(discovery, 'findAvailableElasticIP').mockResolvedValue(null);
|
|
621
|
+
jest.spyOn(discovery, 'isSubnetPrivate')
|
|
622
|
+
.mockImplementation((subnetId) => {
|
|
623
|
+
// subnet-1 is public, subnet-2 is private
|
|
624
|
+
return Promise.resolve(subnetId === 'subnet-2');
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
const result = await discovery.discoverResources({ selfHeal: true });
|
|
628
|
+
|
|
629
|
+
expect(result).toMatchObject({
|
|
630
|
+
subnetConversionRequired: true,
|
|
631
|
+
privateSubnetsWithWrongRoutes: ['subnet-1']
|
|
632
|
+
});
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
it('should handle selfHeal option', async () => {
|
|
636
|
+
const mockVpc = { VpcId: 'vpc-12345678' };
|
|
637
|
+
const mockSubnets = [
|
|
638
|
+
{ SubnetId: 'subnet-public-1' },
|
|
639
|
+
{ SubnetId: 'subnet-public-2' }
|
|
640
|
+
];
|
|
641
|
+
const mockPublicSubnet = { SubnetId: 'subnet-public-3' };
|
|
642
|
+
const mockSecurityGroup = { GroupId: 'sg-12345678' };
|
|
643
|
+
const mockRouteTable = { RouteTableId: 'rtb-12345678' };
|
|
644
|
+
|
|
645
|
+
jest.spyOn(discovery, 'findDefaultVpc').mockResolvedValue(mockVpc);
|
|
646
|
+
jest.spyOn(discovery, 'findPrivateSubnets').mockResolvedValue(mockSubnets);
|
|
647
|
+
jest.spyOn(discovery, 'findPublicSubnets').mockResolvedValue(mockPublicSubnet);
|
|
648
|
+
jest.spyOn(discovery, 'findDefaultSecurityGroup').mockResolvedValue(mockSecurityGroup);
|
|
649
|
+
jest.spyOn(discovery, 'findPrivateRouteTable').mockResolvedValue(mockRouteTable);
|
|
650
|
+
jest.spyOn(discovery, 'findDefaultKmsKey').mockResolvedValue(null);
|
|
651
|
+
jest.spyOn(discovery, 'findExistingNatGateway').mockResolvedValue(null);
|
|
652
|
+
jest.spyOn(discovery, 'findAvailableElasticIP').mockResolvedValue(null);
|
|
653
|
+
jest.spyOn(discovery, 'isSubnetPrivate')
|
|
654
|
+
.mockResolvedValue(false); // All subnets are public
|
|
655
|
+
|
|
656
|
+
const result = await discovery.discoverResources({ selfHeal: true });
|
|
657
|
+
|
|
658
|
+
// Verify that findPrivateSubnets was called with autoConvert=true
|
|
659
|
+
expect(discovery.findPrivateSubnets).toHaveBeenCalledWith('vpc-12345678', true);
|
|
660
|
+
|
|
661
|
+
expect(result).toMatchObject({
|
|
662
|
+
subnetConversionRequired: true,
|
|
663
|
+
privateSubnetsWithWrongRoutes: ['subnet-public-1', 'subnet-public-2']
|
|
664
|
+
});
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
it('should detect NAT Gateway in private subnet in discoverResources', async () => {
|
|
668
|
+
const mockVpc = { VpcId: 'vpc-12345678' };
|
|
669
|
+
const mockSubnets = [
|
|
670
|
+
{ SubnetId: 'subnet-1' },
|
|
671
|
+
{ SubnetId: 'subnet-2' }
|
|
672
|
+
];
|
|
673
|
+
const mockPublicSubnet = { SubnetId: 'subnet-public-1' };
|
|
674
|
+
const mockSecurityGroup = { GroupId: 'sg-12345678' };
|
|
675
|
+
const mockRouteTable = { RouteTableId: 'rtb-12345678' };
|
|
676
|
+
const mockNatGateway = {
|
|
677
|
+
NatGatewayId: 'nat-12345678',
|
|
678
|
+
NatGatewayAddresses: [{ AllocationId: 'eipalloc-12345' }],
|
|
679
|
+
_isInPrivateSubnet: true // NAT is in private subnet
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
jest.spyOn(discovery, 'findDefaultVpc').mockResolvedValue(mockVpc);
|
|
683
|
+
jest.spyOn(discovery, 'findPrivateSubnets').mockResolvedValue(mockSubnets);
|
|
684
|
+
jest.spyOn(discovery, 'findPublicSubnets').mockResolvedValue(mockPublicSubnet);
|
|
685
|
+
jest.spyOn(discovery, 'findDefaultSecurityGroup').mockResolvedValue(mockSecurityGroup);
|
|
686
|
+
jest.spyOn(discovery, 'findPrivateRouteTable').mockResolvedValue(mockRouteTable);
|
|
687
|
+
jest.spyOn(discovery, 'findDefaultKmsKey').mockResolvedValue(null);
|
|
688
|
+
jest.spyOn(discovery, 'findExistingNatGateway').mockResolvedValue(mockNatGateway);
|
|
689
|
+
jest.spyOn(discovery, 'isSubnetPrivate')
|
|
690
|
+
.mockResolvedValueOnce(true)
|
|
691
|
+
.mockResolvedValueOnce(true);
|
|
692
|
+
|
|
693
|
+
const result = await discovery.discoverResources();
|
|
694
|
+
|
|
695
|
+
expect(result).toMatchObject({
|
|
696
|
+
defaultVpcId: 'vpc-12345678',
|
|
697
|
+
existingNatGatewayId: 'nat-12345678',
|
|
698
|
+
natGatewayInPrivateSubnet: true, // Should be true
|
|
699
|
+
subnetConversionRequired: false,
|
|
700
|
+
privateSubnetsWithWrongRoutes: []
|
|
701
|
+
});
|
|
334
702
|
});
|
|
335
703
|
|
|
336
704
|
it('should handle single subnet scenario', async () => {
|
|
337
705
|
const mockVpc = { VpcId: 'vpc-12345678' };
|
|
338
706
|
const mockSubnets = [{ SubnetId: 'subnet-1' }]; // Only one subnet
|
|
707
|
+
const mockPublicSubnet = { SubnetId: 'subnet-public-1' };
|
|
339
708
|
const mockSecurityGroup = { GroupId: 'sg-12345678' };
|
|
340
709
|
const mockRouteTable = { RouteTableId: 'rtb-12345678' };
|
|
341
710
|
const mockKmsArn = 'arn:aws:kms:us-east-1:123456789012:key/12345678';
|
|
342
711
|
|
|
343
712
|
jest.spyOn(discovery, 'findDefaultVpc').mockResolvedValue(mockVpc);
|
|
344
713
|
jest.spyOn(discovery, 'findPrivateSubnets').mockResolvedValue(mockSubnets);
|
|
714
|
+
jest.spyOn(discovery, 'findPublicSubnets').mockResolvedValue(mockPublicSubnet);
|
|
345
715
|
jest.spyOn(discovery, 'findDefaultSecurityGroup').mockResolvedValue(mockSecurityGroup);
|
|
346
716
|
jest.spyOn(discovery, 'findPrivateRouteTable').mockResolvedValue(mockRouteTable);
|
|
347
717
|
jest.spyOn(discovery, 'findDefaultKmsKey').mockResolvedValue(mockKmsArn);
|
|
@@ -1174,7 +1174,8 @@ const composeServerlessDefinition = async (AppDefinition) => {
|
|
|
1174
1174
|
healed: [],
|
|
1175
1175
|
warnings: [],
|
|
1176
1176
|
errors: [],
|
|
1177
|
-
recommendations: []
|
|
1177
|
+
recommendations: [],
|
|
1178
|
+
criticalActions: []
|
|
1178
1179
|
};
|
|
1179
1180
|
|
|
1180
1181
|
// Only heal if selfHeal is explicitly enabled
|
|
@@ -1218,13 +1219,27 @@ const composeServerlessDefinition = async (AppDefinition) => {
|
|
|
1218
1219
|
}
|
|
1219
1220
|
}
|
|
1220
1221
|
|
|
1221
|
-
// Check route table associations
|
|
1222
|
-
if (discoveredResources.privateSubnetsWithWrongRoutes
|
|
1222
|
+
// Check route table associations and subnet conversion requirements
|
|
1223
|
+
if (discoveredResources.privateSubnetsWithWrongRoutes &&
|
|
1224
|
+
discoveredResources.privateSubnetsWithWrongRoutes.length > 0) {
|
|
1223
1225
|
healingReport.warnings.push(
|
|
1224
|
-
`Found ${discoveredResources.privateSubnetsWithWrongRoutes.length}
|
|
1226
|
+
`Found ${discoveredResources.privateSubnetsWithWrongRoutes.length} subnets that are PUBLIC but will be used for Lambda`
|
|
1225
1227
|
);
|
|
1226
1228
|
healingReport.healed.push(
|
|
1227
|
-
'Route tables will be corrected during deployment'
|
|
1229
|
+
'Route tables will be corrected during deployment - converting public subnets to private'
|
|
1230
|
+
);
|
|
1231
|
+
healingReport.criticalActions.push(
|
|
1232
|
+
'SUBNET ISOLATION: Will create separate route tables to ensure Lambda subnets are private'
|
|
1233
|
+
);
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
// Check if subnet conversion is required
|
|
1237
|
+
if (discoveredResources.subnetConversionRequired) {
|
|
1238
|
+
healingReport.warnings.push(
|
|
1239
|
+
'Subnet configuration mismatch detected - Lambda functions require private subnets'
|
|
1240
|
+
);
|
|
1241
|
+
healingReport.healed.push(
|
|
1242
|
+
'Will create proper route table configuration for subnet isolation'
|
|
1228
1243
|
);
|
|
1229
1244
|
}
|
|
1230
1245
|
|
|
@@ -1239,6 +1254,11 @@ const composeServerlessDefinition = async (AppDefinition) => {
|
|
|
1239
1254
|
}
|
|
1240
1255
|
|
|
1241
1256
|
// Log healing report
|
|
1257
|
+
if (healingReport.criticalActions.length > 0) {
|
|
1258
|
+
console.log('๐จ CRITICAL ACTIONS:');
|
|
1259
|
+
healingReport.criticalActions.forEach(action => console.log(` - ${action}`));
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1242
1262
|
if (healingReport.healed.length > 0) {
|
|
1243
1263
|
console.log('โ
Self-healing actions:');
|
|
1244
1264
|
healingReport.healed.forEach(action => console.log(` - ${action}`));
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Test script to verify subnet detection and classification logic
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { AWSDiscovery } = require('./aws-discovery');
|
|
8
|
+
|
|
9
|
+
async function testSubnetLogic() {
|
|
10
|
+
console.log('๐งช Testing Subnet Detection and Classification Logic');
|
|
11
|
+
console.log('โ'.repeat(60));
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const discovery = new AWSDiscovery();
|
|
15
|
+
|
|
16
|
+
// Test with selfHeal enabled
|
|
17
|
+
console.log('\n๐ Testing with selfHeal: true');
|
|
18
|
+
console.log('-'.repeat(40));
|
|
19
|
+
|
|
20
|
+
const resources = await discovery.discoverResources({ selfHeal: true });
|
|
21
|
+
|
|
22
|
+
console.log('\n๐ Results:');
|
|
23
|
+
console.log(` VPC ID: ${resources.defaultVpcId}`);
|
|
24
|
+
console.log(` Private Subnet 1: ${resources.privateSubnetId1}`);
|
|
25
|
+
console.log(` Private Subnet 2: ${resources.privateSubnetId2}`);
|
|
26
|
+
console.log(` Public Subnet: ${resources.publicSubnetId}`);
|
|
27
|
+
console.log(` Conversion Required: ${resources.subnetConversionRequired}`);
|
|
28
|
+
|
|
29
|
+
if (resources.privateSubnetsWithWrongRoutes && resources.privateSubnetsWithWrongRoutes.length > 0) {
|
|
30
|
+
console.log(`\nโ ๏ธ Subnets needing conversion:`);
|
|
31
|
+
resources.privateSubnetsWithWrongRoutes.forEach(subnet => {
|
|
32
|
+
console.log(` - ${subnet}`);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
console.log('\nโ
Test completed successfully!');
|
|
37
|
+
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error('\nโ Test failed:', error.message);
|
|
40
|
+
console.error('\nStack trace:', error.stack);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Run the test
|
|
46
|
+
testSubnetLogic().catch(error => {
|
|
47
|
+
console.error('Unhandled error:', error);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@friggframework/devtools",
|
|
3
3
|
"prettier": "@friggframework/prettier-config",
|
|
4
|
-
"version": "2.0.0--canary.428.
|
|
4
|
+
"version": "2.0.0--canary.428.6b04c24.0",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@aws-sdk/client-ec2": "^3.835.0",
|
|
7
7
|
"@aws-sdk/client-kms": "^3.835.0",
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
"@babel/eslint-parser": "^7.18.9",
|
|
10
10
|
"@babel/parser": "^7.25.3",
|
|
11
11
|
"@babel/traverse": "^7.25.3",
|
|
12
|
-
"@friggframework/schemas": "2.0.0--canary.428.
|
|
13
|
-
"@friggframework/test": "2.0.0--canary.428.
|
|
12
|
+
"@friggframework/schemas": "2.0.0--canary.428.6b04c24.0",
|
|
13
|
+
"@friggframework/test": "2.0.0--canary.428.6b04c24.0",
|
|
14
14
|
"@hapi/boom": "^10.0.1",
|
|
15
15
|
"@inquirer/prompts": "^5.3.8",
|
|
16
16
|
"axios": "^1.7.2",
|
|
@@ -32,8 +32,10 @@
|
|
|
32
32
|
"serverless-http": "^2.7.0"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
|
-
"@friggframework/eslint-config": "2.0.0--canary.428.
|
|
36
|
-
"@friggframework/prettier-config": "2.0.0--canary.428.
|
|
35
|
+
"@friggframework/eslint-config": "2.0.0--canary.428.6b04c24.0",
|
|
36
|
+
"@friggframework/prettier-config": "2.0.0--canary.428.6b04c24.0",
|
|
37
|
+
"aws-sdk-client-mock": "^4.1.0",
|
|
38
|
+
"aws-sdk-client-mock-jest": "^4.1.0",
|
|
37
39
|
"jest": "^30.1.3",
|
|
38
40
|
"prettier": "^2.7.1",
|
|
39
41
|
"serverless": "3.39.0",
|
|
@@ -66,5 +68,5 @@
|
|
|
66
68
|
"publishConfig": {
|
|
67
69
|
"access": "public"
|
|
68
70
|
},
|
|
69
|
-
"gitHead": "
|
|
71
|
+
"gitHead": "6b04c24df1e253fd23afd6acb39ec9b5ad61456f"
|
|
70
72
|
}
|