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