@friggframework/devtools 2.0.0--canary.428.6b04c24.0 ā 2.0.0--canary.428.5c4220d.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.
- package/infrastructure/aws-discovery.js +540 -222
- package/package.json +6 -6
|
@@ -1,22 +1,45 @@
|
|
|
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
|
|
|
@@ -66,26 +89,26 @@ class AWSDiscovery {
|
|
|
66
89
|
Filters: [
|
|
67
90
|
{
|
|
68
91
|
Name: 'is-default',
|
|
69
|
-
Values: ['true']
|
|
70
|
-
}
|
|
71
|
-
]
|
|
92
|
+
Values: ['true'],
|
|
93
|
+
},
|
|
94
|
+
],
|
|
72
95
|
});
|
|
73
|
-
|
|
96
|
+
|
|
74
97
|
const response = await this.ec2Client.send(command);
|
|
75
|
-
|
|
98
|
+
|
|
76
99
|
if (response.Vpcs && response.Vpcs.length > 0) {
|
|
77
100
|
return response.Vpcs[0];
|
|
78
101
|
}
|
|
79
|
-
|
|
102
|
+
|
|
80
103
|
// If no default VPC, get the first available VPC
|
|
81
104
|
const allVpcsCommand = new DescribeVpcsCommand({});
|
|
82
105
|
const allVpcsResponse = await this.ec2Client.send(allVpcsCommand);
|
|
83
|
-
|
|
106
|
+
|
|
84
107
|
if (allVpcsResponse.Vpcs && allVpcsResponse.Vpcs.length > 0) {
|
|
85
108
|
console.log('No default VPC found, using first available VPC');
|
|
86
109
|
return allVpcsResponse.Vpcs[0];
|
|
87
110
|
}
|
|
88
|
-
|
|
111
|
+
|
|
89
112
|
throw new Error('No VPC found in the account');
|
|
90
113
|
} catch (error) {
|
|
91
114
|
console.error('Error finding default VPC:', error.message);
|
|
@@ -106,9 +129,9 @@ class AWSDiscovery {
|
|
|
106
129
|
Filters: [
|
|
107
130
|
{
|
|
108
131
|
Name: 'vpc-id',
|
|
109
|
-
Values: [vpcId]
|
|
110
|
-
}
|
|
111
|
-
]
|
|
132
|
+
Values: [vpcId],
|
|
133
|
+
},
|
|
134
|
+
],
|
|
112
135
|
});
|
|
113
136
|
|
|
114
137
|
const response = await this.ec2Client.send(command);
|
|
@@ -117,7 +140,9 @@ class AWSDiscovery {
|
|
|
117
140
|
throw new Error(`No subnets found in VPC ${vpcId}`);
|
|
118
141
|
}
|
|
119
142
|
|
|
120
|
-
console.log(
|
|
143
|
+
console.log(
|
|
144
|
+
`\nš Analyzing ${response.Subnets.length} subnets in VPC ${vpcId}...`
|
|
145
|
+
);
|
|
121
146
|
|
|
122
147
|
// Categorize subnets by their actual routing
|
|
123
148
|
const privateSubnets = [];
|
|
@@ -128,10 +153,14 @@ class AWSDiscovery {
|
|
|
128
153
|
const isPrivate = await this.isSubnetPrivate(subnet.SubnetId);
|
|
129
154
|
if (isPrivate) {
|
|
130
155
|
privateSubnets.push(subnet);
|
|
131
|
-
console.log(
|
|
156
|
+
console.log(
|
|
157
|
+
` š Private subnet: ${subnet.SubnetId} (AZ: ${subnet.AvailabilityZone})`
|
|
158
|
+
);
|
|
132
159
|
} else {
|
|
133
160
|
publicSubnets.push(subnet);
|
|
134
|
-
console.log(
|
|
161
|
+
console.log(
|
|
162
|
+
` š Public subnet: ${subnet.SubnetId} (AZ: ${subnet.AvailabilityZone})`
|
|
163
|
+
);
|
|
135
164
|
}
|
|
136
165
|
}
|
|
137
166
|
|
|
@@ -141,15 +170,21 @@ class AWSDiscovery {
|
|
|
141
170
|
|
|
142
171
|
// If we have at least 2 private subnets, use them
|
|
143
172
|
if (privateSubnets.length >= 2) {
|
|
144
|
-
console.log(
|
|
173
|
+
console.log(
|
|
174
|
+
`ā
Found ${privateSubnets.length} private subnets for Lambda deployment`
|
|
175
|
+
);
|
|
145
176
|
return privateSubnets.slice(0, 2);
|
|
146
177
|
}
|
|
147
178
|
|
|
148
179
|
// If we have 1 private subnet, we need at least one more
|
|
149
180
|
if (privateSubnets.length === 1) {
|
|
150
|
-
console.warn(
|
|
181
|
+
console.warn(
|
|
182
|
+
`ā ļø Only 1 private subnet found. Need at least 2 for high availability.`
|
|
183
|
+
);
|
|
151
184
|
if (publicSubnets.length > 0 && autoConvert) {
|
|
152
|
-
console.log(
|
|
185
|
+
console.log(
|
|
186
|
+
`š Will convert 1 public subnet to private for high availability...`
|
|
187
|
+
);
|
|
153
188
|
// Note: The actual conversion happens in the serverless template
|
|
154
189
|
}
|
|
155
190
|
// Return what we have - mix of private and public if needed
|
|
@@ -158,38 +193,62 @@ class AWSDiscovery {
|
|
|
158
193
|
|
|
159
194
|
// No private subnets found at all - this is a problem!
|
|
160
195
|
if (privateSubnets.length === 0 && publicSubnets.length > 0) {
|
|
161
|
-
console.error(
|
|
162
|
-
|
|
196
|
+
console.error(
|
|
197
|
+
`ā CRITICAL: No private subnets found, but ${publicSubnets.length} public subnets exist`
|
|
198
|
+
);
|
|
199
|
+
console.error(
|
|
200
|
+
`ā Lambda functions should NOT be deployed in public subnets!`
|
|
201
|
+
);
|
|
163
202
|
|
|
164
203
|
if (autoConvert && publicSubnets.length >= 3) {
|
|
165
|
-
console.log(
|
|
166
|
-
|
|
167
|
-
|
|
204
|
+
console.log(
|
|
205
|
+
`\nš§ AUTO-CONVERSION: Will configure subnets for proper isolation...`
|
|
206
|
+
);
|
|
207
|
+
console.log(
|
|
208
|
+
` - Keeping ${publicSubnets[0].SubnetId} as public (for NAT Gateway)`
|
|
209
|
+
);
|
|
210
|
+
console.log(
|
|
211
|
+
` - Converting ${publicSubnets[1].SubnetId} to private (for Lambda)`
|
|
212
|
+
);
|
|
168
213
|
if (publicSubnets[2]) {
|
|
169
|
-
console.log(
|
|
214
|
+
console.log(
|
|
215
|
+
` - Converting ${publicSubnets[2].SubnetId} to private (for Lambda)`
|
|
216
|
+
);
|
|
170
217
|
}
|
|
171
218
|
|
|
172
219
|
// Return subnets that SHOULD be private (indexes 1 and 2)
|
|
173
220
|
// The actual conversion happens in the serverless template
|
|
174
221
|
return publicSubnets.slice(1, 3);
|
|
175
222
|
} else if (autoConvert && publicSubnets.length >= 2) {
|
|
176
|
-
console.log(
|
|
177
|
-
|
|
223
|
+
console.log(
|
|
224
|
+
`\nš§ AUTO-CONVERSION: Only ${publicSubnets.length} subnets available`
|
|
225
|
+
);
|
|
226
|
+
console.log(
|
|
227
|
+
` - Will need to create new subnets or reconfigure existing ones`
|
|
228
|
+
);
|
|
178
229
|
// Return what we have but flag for conversion
|
|
179
230
|
return publicSubnets.slice(0, 2);
|
|
180
231
|
} else {
|
|
181
232
|
console.error(`\nā ļø CONFIGURATION ERROR:`);
|
|
182
|
-
console.error(
|
|
183
|
-
|
|
233
|
+
console.error(
|
|
234
|
+
` Found ${publicSubnets.length} public subnets but no private subnets.`
|
|
235
|
+
);
|
|
236
|
+
console.error(
|
|
237
|
+
` Lambda functions require private subnets for security.`
|
|
238
|
+
);
|
|
184
239
|
console.error(`\n Options:`);
|
|
185
|
-
console.error(
|
|
240
|
+
console.error(
|
|
241
|
+
` 1. Enable selfHeal: true in vpc configuration`
|
|
242
|
+
);
|
|
186
243
|
console.error(` 2. Create private subnets manually`);
|
|
187
|
-
console.error(
|
|
244
|
+
console.error(
|
|
245
|
+
` 3. Set subnets.management: 'create' to create new private subnets`
|
|
246
|
+
);
|
|
188
247
|
|
|
189
248
|
throw new Error(
|
|
190
249
|
`No private subnets found in VPC ${vpcId}. ` +
|
|
191
|
-
|
|
192
|
-
|
|
250
|
+
`Found ${publicSubnets.length} public subnets. ` +
|
|
251
|
+
`Lambda requires private subnets. Enable selfHeal or create private subnets.`
|
|
193
252
|
);
|
|
194
253
|
}
|
|
195
254
|
}
|
|
@@ -221,32 +280,37 @@ class AWSDiscovery {
|
|
|
221
280
|
try {
|
|
222
281
|
// First, get the subnet details to find its VPC
|
|
223
282
|
const subnetCommand = new DescribeSubnetsCommand({
|
|
224
|
-
SubnetIds: [subnetId]
|
|
283
|
+
SubnetIds: [subnetId],
|
|
225
284
|
});
|
|
226
285
|
const subnetResponse = await this.ec2Client.send(subnetCommand);
|
|
227
|
-
|
|
228
|
-
if (
|
|
286
|
+
|
|
287
|
+
if (
|
|
288
|
+
!subnetResponse.Subnets ||
|
|
289
|
+
subnetResponse.Subnets.length === 0
|
|
290
|
+
) {
|
|
229
291
|
throw new Error(`Subnet ${subnetId} not found`);
|
|
230
292
|
}
|
|
231
|
-
|
|
293
|
+
|
|
232
294
|
const subnet = subnetResponse.Subnets[0];
|
|
233
295
|
const vpcId = subnet.VpcId;
|
|
234
|
-
|
|
296
|
+
|
|
235
297
|
// Get all route tables for this VPC
|
|
236
298
|
const routeTablesCommand = new DescribeRouteTablesCommand({
|
|
237
299
|
Filters: [
|
|
238
300
|
{
|
|
239
301
|
Name: 'vpc-id',
|
|
240
|
-
Values: [vpcId]
|
|
241
|
-
}
|
|
242
|
-
]
|
|
302
|
+
Values: [vpcId],
|
|
303
|
+
},
|
|
304
|
+
],
|
|
243
305
|
});
|
|
244
|
-
|
|
245
|
-
const routeTablesResponse = await this.ec2Client.send(
|
|
246
|
-
|
|
306
|
+
|
|
307
|
+
const routeTablesResponse = await this.ec2Client.send(
|
|
308
|
+
routeTablesCommand
|
|
309
|
+
);
|
|
310
|
+
|
|
247
311
|
// Find the route table for this subnet
|
|
248
312
|
let routeTable = null;
|
|
249
|
-
|
|
313
|
+
|
|
250
314
|
// First check for explicit association
|
|
251
315
|
for (const rt of routeTablesResponse.RouteTables || []) {
|
|
252
316
|
for (const assoc of rt.Associations || []) {
|
|
@@ -257,7 +321,7 @@ class AWSDiscovery {
|
|
|
257
321
|
}
|
|
258
322
|
if (routeTable) break;
|
|
259
323
|
}
|
|
260
|
-
|
|
324
|
+
|
|
261
325
|
// If no explicit association, use the main route table
|
|
262
326
|
if (!routeTable) {
|
|
263
327
|
for (const rt of routeTablesResponse.RouteTables || []) {
|
|
@@ -270,12 +334,12 @@ class AWSDiscovery {
|
|
|
270
334
|
if (routeTable) break;
|
|
271
335
|
}
|
|
272
336
|
}
|
|
273
|
-
|
|
337
|
+
|
|
274
338
|
if (!routeTable) {
|
|
275
339
|
console.warn(`No route table found for subnet ${subnetId}`);
|
|
276
340
|
return true; // Default to private for safety
|
|
277
341
|
}
|
|
278
|
-
|
|
342
|
+
|
|
279
343
|
// Check if route table has a route to an Internet Gateway
|
|
280
344
|
let hasIgwRoute = false;
|
|
281
345
|
let gatewayId = null;
|
|
@@ -290,14 +354,21 @@ class AWSDiscovery {
|
|
|
290
354
|
|
|
291
355
|
// Enhanced logging for validation
|
|
292
356
|
if (hasIgwRoute) {
|
|
293
|
-
console.log(
|
|
357
|
+
console.log(
|
|
358
|
+
`ā
Subnet ${subnetId} is PUBLIC (has route to IGW ${gatewayId})`
|
|
359
|
+
);
|
|
294
360
|
return false; // It's a public subnet
|
|
295
361
|
} else {
|
|
296
|
-
console.log(
|
|
362
|
+
console.log(
|
|
363
|
+
`š Subnet ${subnetId} is PRIVATE (no IGW route found)`
|
|
364
|
+
);
|
|
297
365
|
return true; // No IGW route found, it's private
|
|
298
366
|
}
|
|
299
367
|
} catch (error) {
|
|
300
|
-
console.warn(
|
|
368
|
+
console.warn(
|
|
369
|
+
`Could not determine if subnet ${subnetId} is private:`,
|
|
370
|
+
error
|
|
371
|
+
);
|
|
301
372
|
return true; // Default to private for safety
|
|
302
373
|
}
|
|
303
374
|
}
|
|
@@ -315,17 +386,20 @@ class AWSDiscovery {
|
|
|
315
386
|
Filters: [
|
|
316
387
|
{
|
|
317
388
|
Name: 'vpc-id',
|
|
318
|
-
Values: [vpcId]
|
|
389
|
+
Values: [vpcId],
|
|
319
390
|
},
|
|
320
391
|
{
|
|
321
392
|
Name: 'group-name',
|
|
322
|
-
Values: ['frigg-lambda-sg']
|
|
323
|
-
}
|
|
324
|
-
]
|
|
393
|
+
Values: ['frigg-lambda-sg'],
|
|
394
|
+
},
|
|
395
|
+
],
|
|
325
396
|
});
|
|
326
|
-
|
|
397
|
+
|
|
327
398
|
const friggResponse = await this.ec2Client.send(friggSgCommand);
|
|
328
|
-
if (
|
|
399
|
+
if (
|
|
400
|
+
friggResponse.SecurityGroups &&
|
|
401
|
+
friggResponse.SecurityGroups.length > 0
|
|
402
|
+
) {
|
|
329
403
|
return friggResponse.SecurityGroups[0];
|
|
330
404
|
}
|
|
331
405
|
|
|
@@ -334,20 +408,23 @@ class AWSDiscovery {
|
|
|
334
408
|
Filters: [
|
|
335
409
|
{
|
|
336
410
|
Name: 'vpc-id',
|
|
337
|
-
Values: [vpcId]
|
|
411
|
+
Values: [vpcId],
|
|
338
412
|
},
|
|
339
413
|
{
|
|
340
414
|
Name: 'group-name',
|
|
341
|
-
Values: ['default']
|
|
342
|
-
}
|
|
343
|
-
]
|
|
415
|
+
Values: ['default'],
|
|
416
|
+
},
|
|
417
|
+
],
|
|
344
418
|
});
|
|
345
|
-
|
|
419
|
+
|
|
346
420
|
const defaultResponse = await this.ec2Client.send(defaultSgCommand);
|
|
347
|
-
if (
|
|
421
|
+
if (
|
|
422
|
+
defaultResponse.SecurityGroups &&
|
|
423
|
+
defaultResponse.SecurityGroups.length > 0
|
|
424
|
+
) {
|
|
348
425
|
return defaultResponse.SecurityGroups[0];
|
|
349
426
|
}
|
|
350
|
-
|
|
427
|
+
|
|
351
428
|
throw new Error(`No security group found for VPC ${vpcId}`);
|
|
352
429
|
} catch (error) {
|
|
353
430
|
console.error('Error finding default security group:', error);
|
|
@@ -367,9 +444,9 @@ class AWSDiscovery {
|
|
|
367
444
|
Filters: [
|
|
368
445
|
{
|
|
369
446
|
Name: 'vpc-id',
|
|
370
|
-
Values: [vpcId]
|
|
371
|
-
}
|
|
372
|
-
]
|
|
447
|
+
Values: [vpcId],
|
|
448
|
+
},
|
|
449
|
+
],
|
|
373
450
|
});
|
|
374
451
|
|
|
375
452
|
const response = await this.ec2Client.send(command);
|
|
@@ -391,14 +468,22 @@ class AWSDiscovery {
|
|
|
391
468
|
|
|
392
469
|
if (publicSubnets.length === 0) {
|
|
393
470
|
// If no public subnets found, we need to create one or inform the user
|
|
394
|
-
console.warn(
|
|
395
|
-
|
|
396
|
-
|
|
471
|
+
console.warn(
|
|
472
|
+
`WARNING: No public subnets found in VPC ${vpcId}`
|
|
473
|
+
);
|
|
474
|
+
console.warn(
|
|
475
|
+
'A public subnet with Internet Gateway route is required for NAT Gateway placement'
|
|
476
|
+
);
|
|
477
|
+
console.warn(
|
|
478
|
+
'Please create a public subnet or use VPC endpoints instead'
|
|
479
|
+
);
|
|
397
480
|
return null; // Return null instead of throwing to allow graceful handling
|
|
398
481
|
}
|
|
399
482
|
|
|
400
483
|
// Return first public subnet for NAT Gateway
|
|
401
|
-
console.log(
|
|
484
|
+
console.log(
|
|
485
|
+
`Found ${publicSubnets.length} public subnets, using ${publicSubnets[0].SubnetId} for NAT Gateway`
|
|
486
|
+
);
|
|
402
487
|
return publicSubnets[0];
|
|
403
488
|
} catch (error) {
|
|
404
489
|
console.error('Error finding public subnets:', error);
|
|
@@ -418,13 +503,13 @@ class AWSDiscovery {
|
|
|
418
503
|
Filters: [
|
|
419
504
|
{
|
|
420
505
|
Name: 'vpc-id',
|
|
421
|
-
Values: [vpcId]
|
|
422
|
-
}
|
|
423
|
-
]
|
|
506
|
+
Values: [vpcId],
|
|
507
|
+
},
|
|
508
|
+
],
|
|
424
509
|
});
|
|
425
|
-
|
|
510
|
+
|
|
426
511
|
const response = await this.ec2Client.send(command);
|
|
427
|
-
|
|
512
|
+
|
|
428
513
|
if (!response.RouteTables || response.RouteTables.length === 0) {
|
|
429
514
|
throw new Error(`No route tables found for VPC ${vpcId}`);
|
|
430
515
|
}
|
|
@@ -462,13 +547,13 @@ class AWSDiscovery {
|
|
|
462
547
|
Filter: [
|
|
463
548
|
{
|
|
464
549
|
Name: 'vpc-id',
|
|
465
|
-
Values: [vpcId]
|
|
550
|
+
Values: [vpcId],
|
|
466
551
|
},
|
|
467
552
|
{
|
|
468
553
|
Name: 'state',
|
|
469
|
-
Values: ['available']
|
|
470
|
-
}
|
|
471
|
-
]
|
|
554
|
+
Values: ['available'],
|
|
555
|
+
},
|
|
556
|
+
],
|
|
472
557
|
});
|
|
473
558
|
|
|
474
559
|
const response = await this.ec2Client.send(command);
|
|
@@ -476,14 +561,24 @@ class AWSDiscovery {
|
|
|
476
561
|
if (response.NatGateways && response.NatGateways.length > 0) {
|
|
477
562
|
// Sort NAT Gateways to prioritize Frigg-managed ones
|
|
478
563
|
const sortedNatGateways = response.NatGateways.sort((a, b) => {
|
|
479
|
-
const aIsFrigg =
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
564
|
+
const aIsFrigg =
|
|
565
|
+
a.Tags &&
|
|
566
|
+
a.Tags.some(
|
|
567
|
+
(tag) =>
|
|
568
|
+
(tag.Key === 'ManagedBy' &&
|
|
569
|
+
tag.Value === 'Frigg') ||
|
|
570
|
+
(tag.Key === 'Name' &&
|
|
571
|
+
tag.Value.includes('frigg'))
|
|
572
|
+
);
|
|
573
|
+
const bIsFrigg =
|
|
574
|
+
b.Tags &&
|
|
575
|
+
b.Tags.some(
|
|
576
|
+
(tag) =>
|
|
577
|
+
(tag.Key === 'ManagedBy' &&
|
|
578
|
+
tag.Value === 'Frigg') ||
|
|
579
|
+
(tag.Key === 'Name' &&
|
|
580
|
+
tag.Value.includes('frigg'))
|
|
581
|
+
);
|
|
487
582
|
|
|
488
583
|
if (aIsFrigg && !bIsFrigg) return -1;
|
|
489
584
|
if (!aIsFrigg && bIsFrigg) return 1;
|
|
@@ -496,46 +591,71 @@ class AWSDiscovery {
|
|
|
496
591
|
const isPrivate = await this.isSubnetPrivate(subnetId);
|
|
497
592
|
|
|
498
593
|
// Check if it's a Frigg-managed NAT Gateway
|
|
499
|
-
const isFriggNat =
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
594
|
+
const isFriggNat =
|
|
595
|
+
natGateway.Tags &&
|
|
596
|
+
natGateway.Tags.some(
|
|
597
|
+
(tag) =>
|
|
598
|
+
(tag.Key === 'ManagedBy' &&
|
|
599
|
+
tag.Value === 'Frigg') ||
|
|
600
|
+
(tag.Key === 'Name' &&
|
|
601
|
+
tag.Value.includes('frigg'))
|
|
602
|
+
);
|
|
503
603
|
|
|
504
604
|
if (isPrivate) {
|
|
505
605
|
// NAT Gateway appears to be in a private subnet
|
|
506
606
|
// This could be due to route table misconfiguration
|
|
507
|
-
console.warn(
|
|
607
|
+
console.warn(
|
|
608
|
+
`WARNING: NAT Gateway ${natGateway.NatGatewayId} is in subnet ${subnetId} which appears to be private`
|
|
609
|
+
);
|
|
508
610
|
|
|
509
611
|
if (isFriggNat) {
|
|
510
|
-
console.warn(
|
|
511
|
-
|
|
612
|
+
console.warn(
|
|
613
|
+
'This is a Frigg-managed NAT Gateway that may have been misconfigured by route table changes'
|
|
614
|
+
);
|
|
615
|
+
console.warn(
|
|
616
|
+
'Consider enabling selfHeal: true to fix this automatically'
|
|
617
|
+
);
|
|
512
618
|
// Return it anyway if it's Frigg-managed - we can fix the routes
|
|
513
619
|
// Mark that it's in a private subnet
|
|
514
620
|
natGateway._isInPrivateSubnet = true;
|
|
515
621
|
return natGateway;
|
|
516
622
|
} else {
|
|
517
|
-
console.warn(
|
|
518
|
-
|
|
623
|
+
console.warn(
|
|
624
|
+
'NAT Gateways MUST be placed in public subnets with Internet Gateway routes'
|
|
625
|
+
);
|
|
626
|
+
console.warn(
|
|
627
|
+
'Skipping this misconfigured NAT Gateway...'
|
|
628
|
+
);
|
|
519
629
|
continue; // Skip non-Frigg NAT Gateways in private subnets
|
|
520
630
|
}
|
|
521
631
|
}
|
|
522
632
|
|
|
523
633
|
if (isFriggNat) {
|
|
524
|
-
console.log(
|
|
634
|
+
console.log(
|
|
635
|
+
`Found existing Frigg-managed NAT Gateway: ${natGateway.NatGatewayId}`
|
|
636
|
+
);
|
|
525
637
|
natGateway._isInPrivateSubnet = false;
|
|
526
638
|
return natGateway;
|
|
527
639
|
}
|
|
528
640
|
|
|
529
641
|
// Return first valid NAT Gateway that's in a public subnet
|
|
530
|
-
console.log(
|
|
642
|
+
console.log(
|
|
643
|
+
`Found existing NAT Gateway in public subnet: ${natGateway.NatGatewayId}`
|
|
644
|
+
);
|
|
531
645
|
natGateway._isInPrivateSubnet = false;
|
|
532
646
|
return natGateway;
|
|
533
647
|
}
|
|
534
648
|
|
|
535
649
|
// All non-Frigg NAT Gateways are in private subnets
|
|
536
|
-
console.error(
|
|
537
|
-
|
|
538
|
-
|
|
650
|
+
console.error(
|
|
651
|
+
`ERROR: Found ${response.NatGateways.length} NAT Gateway(s) but all non-Frigg ones are in private subnets!`
|
|
652
|
+
);
|
|
653
|
+
console.error(
|
|
654
|
+
'These NAT Gateways will not provide internet connectivity without route table fixes'
|
|
655
|
+
);
|
|
656
|
+
console.error(
|
|
657
|
+
'Enable selfHeal: true to fix automatically or create a new NAT Gateway'
|
|
658
|
+
);
|
|
539
659
|
return null; // Return null to trigger creation of new NAT Gateway
|
|
540
660
|
}
|
|
541
661
|
|
|
@@ -554,31 +674,42 @@ class AWSDiscovery {
|
|
|
554
674
|
try {
|
|
555
675
|
const command = new DescribeAddressesCommand({});
|
|
556
676
|
const response = await this.ec2Client.send(command);
|
|
557
|
-
|
|
677
|
+
|
|
558
678
|
if (response.Addresses && response.Addresses.length > 0) {
|
|
559
679
|
// Find an unassociated EIP first
|
|
560
|
-
const availableEIP = response.Addresses.find(
|
|
561
|
-
|
|
680
|
+
const availableEIP = response.Addresses.find(
|
|
681
|
+
(eip) =>
|
|
682
|
+
!eip.AssociationId &&
|
|
683
|
+
!eip.InstanceId &&
|
|
684
|
+
!eip.NetworkInterfaceId
|
|
562
685
|
);
|
|
563
|
-
|
|
686
|
+
|
|
564
687
|
if (availableEIP) {
|
|
565
|
-
console.log(
|
|
688
|
+
console.log(
|
|
689
|
+
`Found available Elastic IP: ${availableEIP.AllocationId}`
|
|
690
|
+
);
|
|
566
691
|
return availableEIP;
|
|
567
692
|
}
|
|
568
|
-
|
|
693
|
+
|
|
569
694
|
// Check for EIPs tagged for Frigg
|
|
570
|
-
const friggEIP = response.Addresses.find(
|
|
571
|
-
eip
|
|
572
|
-
|
|
573
|
-
|
|
695
|
+
const friggEIP = response.Addresses.find(
|
|
696
|
+
(eip) =>
|
|
697
|
+
eip.Tags &&
|
|
698
|
+
eip.Tags.some(
|
|
699
|
+
(tag) =>
|
|
700
|
+
tag.Key === 'Name' &&
|
|
701
|
+
tag.Value.includes('frigg')
|
|
702
|
+
)
|
|
574
703
|
);
|
|
575
|
-
|
|
704
|
+
|
|
576
705
|
if (friggEIP) {
|
|
577
|
-
console.log(
|
|
706
|
+
console.log(
|
|
707
|
+
`Found Frigg-tagged Elastic IP: ${friggEIP.AllocationId}`
|
|
708
|
+
);
|
|
578
709
|
return friggEIP;
|
|
579
710
|
}
|
|
580
711
|
}
|
|
581
|
-
|
|
712
|
+
|
|
582
713
|
return null;
|
|
583
714
|
} catch (error) {
|
|
584
715
|
console.warn('Error finding available Elastic IP:', error.message);
|
|
@@ -591,38 +722,136 @@ class AWSDiscovery {
|
|
|
591
722
|
* @returns {Promise<string|null>} KMS key ARN or null if no key found
|
|
592
723
|
*/
|
|
593
724
|
async findDefaultKmsKey() {
|
|
725
|
+
console.log('KMS Discovery Starting...');
|
|
594
726
|
try {
|
|
727
|
+
// Log AWS account and region info for verification
|
|
728
|
+
console.log(`[KMS Discovery] Running in region: ${this.region}`);
|
|
729
|
+
try {
|
|
730
|
+
const accountId = await this.getAccountId();
|
|
731
|
+
console.log(`[KMS Discovery] AWS Account ID: ${accountId}`);
|
|
732
|
+
} catch (error) {
|
|
733
|
+
console.warn(
|
|
734
|
+
'[KMS Discovery] Could not retrieve account ID:',
|
|
735
|
+
error.message
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
|
|
595
739
|
const command = new ListKeysCommand({});
|
|
596
740
|
const response = await this.kmsClient.send(command);
|
|
597
|
-
|
|
741
|
+
|
|
598
742
|
if (!response.Keys || response.Keys.length === 0) {
|
|
599
|
-
console.log('No KMS keys found in account');
|
|
743
|
+
console.log('[KMS Discovery] No KMS keys found in account');
|
|
600
744
|
return null;
|
|
601
745
|
}
|
|
602
746
|
|
|
747
|
+
console.log(
|
|
748
|
+
`[KMS Discovery] Found ${response.Keys.length} total keys in account`
|
|
749
|
+
);
|
|
750
|
+
let keysExamined = 0;
|
|
751
|
+
let customerManagedKeys = 0;
|
|
752
|
+
let enabledKeys = 0;
|
|
753
|
+
let pendingDeletionKeys = 0;
|
|
754
|
+
|
|
603
755
|
// Look for customer managed keys first
|
|
604
756
|
for (const key of response.Keys) {
|
|
605
757
|
try {
|
|
606
|
-
const describeCommand = new DescribeKeyCommand({
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
758
|
+
const describeCommand = new DescribeKeyCommand({
|
|
759
|
+
KeyId: key.KeyId,
|
|
760
|
+
});
|
|
761
|
+
const keyDetails = await this.kmsClient.send(
|
|
762
|
+
describeCommand
|
|
763
|
+
);
|
|
764
|
+
keysExamined++;
|
|
765
|
+
|
|
766
|
+
if (keyDetails.KeyMetadata) {
|
|
767
|
+
const metadata = keyDetails.KeyMetadata;
|
|
768
|
+
|
|
769
|
+
// Log detailed key information
|
|
770
|
+
console.log(`[KMS Discovery] Key ${key.KeyId}:`, {
|
|
771
|
+
KeyManager: metadata.KeyManager,
|
|
772
|
+
KeyState: metadata.KeyState,
|
|
773
|
+
Enabled: metadata.Enabled,
|
|
774
|
+
DeletionDate:
|
|
775
|
+
metadata.DeletionDate ||
|
|
776
|
+
'Not scheduled for deletion',
|
|
777
|
+
Arn: metadata.Arn,
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
if (metadata.KeyManager === 'CUSTOMER') {
|
|
781
|
+
customerManagedKeys++;
|
|
782
|
+
|
|
783
|
+
if (metadata.KeyState === 'Enabled') {
|
|
784
|
+
enabledKeys++;
|
|
785
|
+
} else if (
|
|
786
|
+
metadata.KeyState === 'PendingDeletion'
|
|
787
|
+
) {
|
|
788
|
+
pendingDeletionKeys++;
|
|
789
|
+
console.warn(
|
|
790
|
+
`[KMS Discovery] Skipping key ${key.KeyId} - State: PendingDeletion, DeletionDate: ${metadata.DeletionDate}`
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// Explicitly check for enabled state AND absence of deletion
|
|
795
|
+
if (
|
|
796
|
+
metadata.KeyManager === 'CUSTOMER' &&
|
|
797
|
+
metadata.KeyState === 'Enabled' &&
|
|
798
|
+
!metadata.DeletionDate
|
|
799
|
+
) {
|
|
800
|
+
console.log(
|
|
801
|
+
`[KMS Discovery] Found eligible customer managed KMS key: ${metadata.Arn}`
|
|
802
|
+
);
|
|
803
|
+
return metadata.Arn;
|
|
804
|
+
} else if (
|
|
805
|
+
metadata.KeyManager === 'CUSTOMER' &&
|
|
806
|
+
metadata.KeyState === 'Enabled' &&
|
|
807
|
+
metadata.DeletionDate
|
|
808
|
+
) {
|
|
809
|
+
// This shouldn't happen according to AWS docs, but log it if it does
|
|
810
|
+
console.error(
|
|
811
|
+
`[KMS Discovery] WARNING: Key ${key.KeyId} has KeyState='Enabled' but DeletionDate is set: ${metadata.DeletionDate}`
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
614
815
|
}
|
|
615
816
|
} catch (error) {
|
|
616
817
|
// Continue to next key if we can't describe this one
|
|
617
|
-
console.warn(
|
|
818
|
+
console.warn(
|
|
819
|
+
`[KMS Discovery] Could not describe key ${key.KeyId}:`,
|
|
820
|
+
error.message
|
|
821
|
+
);
|
|
618
822
|
continue;
|
|
619
823
|
}
|
|
620
824
|
}
|
|
621
825
|
|
|
622
|
-
|
|
826
|
+
// Summary logging
|
|
827
|
+
console.log('[KMS Discovery] Summary:', {
|
|
828
|
+
totalKeys: response.Keys.length,
|
|
829
|
+
keysExamined: keysExamined,
|
|
830
|
+
customerManagedKeys: customerManagedKeys,
|
|
831
|
+
enabledKeys: enabledKeys,
|
|
832
|
+
pendingDeletionKeys: pendingDeletionKeys,
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
if (customerManagedKeys === 0) {
|
|
836
|
+
console.log(
|
|
837
|
+
'[KMS Discovery] No customer managed KMS keys found in account'
|
|
838
|
+
);
|
|
839
|
+
} else if (enabledKeys === 0) {
|
|
840
|
+
console.warn(
|
|
841
|
+
'[KMS Discovery] Found customer managed keys but none are in Enabled state'
|
|
842
|
+
);
|
|
843
|
+
} else {
|
|
844
|
+
console.warn(
|
|
845
|
+
'[KMS Discovery] Found enabled customer managed keys but none met all criteria'
|
|
846
|
+
);
|
|
847
|
+
}
|
|
848
|
+
|
|
623
849
|
return null;
|
|
624
850
|
} catch (error) {
|
|
625
|
-
console.error(
|
|
851
|
+
console.error(
|
|
852
|
+
'[KMS Discovery] Error finding default KMS key:',
|
|
853
|
+
error
|
|
854
|
+
);
|
|
626
855
|
return null;
|
|
627
856
|
}
|
|
628
857
|
}
|
|
@@ -638,23 +867,26 @@ class AWSDiscovery {
|
|
|
638
867
|
natGateways: [],
|
|
639
868
|
elasticIps: [],
|
|
640
869
|
subnets: [],
|
|
641
|
-
routeTables: []
|
|
870
|
+
routeTables: [],
|
|
642
871
|
};
|
|
643
872
|
|
|
644
873
|
// Find NAT Gateways with Frigg tags
|
|
645
874
|
const natCommand = new DescribeNatGatewaysCommand({
|
|
646
875
|
Filter: [
|
|
647
876
|
{ Name: 'vpc-id', Values: [vpcId] },
|
|
648
|
-
{ Name: 'state', Values: ['available'] }
|
|
649
|
-
]
|
|
877
|
+
{ Name: 'state', Values: ['available'] },
|
|
878
|
+
],
|
|
650
879
|
});
|
|
651
880
|
const natResponse = await this.ec2Client.send(natCommand);
|
|
652
881
|
|
|
653
882
|
if (natResponse.NatGateways) {
|
|
654
|
-
resources.natGateways = natResponse.NatGateways.filter(
|
|
655
|
-
nat
|
|
656
|
-
|
|
657
|
-
|
|
883
|
+
resources.natGateways = natResponse.NatGateways.filter(
|
|
884
|
+
(nat) =>
|
|
885
|
+
nat.Tags &&
|
|
886
|
+
nat.Tags.some(
|
|
887
|
+
(tag) =>
|
|
888
|
+
tag.Key === 'ManagedBy' && tag.Value === 'Frigg'
|
|
889
|
+
)
|
|
658
890
|
);
|
|
659
891
|
}
|
|
660
892
|
|
|
@@ -663,26 +895,30 @@ class AWSDiscovery {
|
|
|
663
895
|
const eipResponse = await this.ec2Client.send(eipCommand);
|
|
664
896
|
|
|
665
897
|
if (eipResponse.Addresses) {
|
|
666
|
-
resources.elasticIps = eipResponse.Addresses.filter(
|
|
667
|
-
eip
|
|
668
|
-
|
|
669
|
-
|
|
898
|
+
resources.elasticIps = eipResponse.Addresses.filter(
|
|
899
|
+
(eip) =>
|
|
900
|
+
eip.Tags &&
|
|
901
|
+
eip.Tags.some(
|
|
902
|
+
(tag) =>
|
|
903
|
+
tag.Key === 'ManagedBy' && tag.Value === 'Frigg'
|
|
904
|
+
)
|
|
670
905
|
);
|
|
671
906
|
}
|
|
672
907
|
|
|
673
908
|
// Find Route Tables with Frigg tags
|
|
674
909
|
const rtCommand = new DescribeRouteTablesCommand({
|
|
675
|
-
Filters: [
|
|
676
|
-
{ Name: 'vpc-id', Values: [vpcId] }
|
|
677
|
-
]
|
|
910
|
+
Filters: [{ Name: 'vpc-id', Values: [vpcId] }],
|
|
678
911
|
});
|
|
679
912
|
const rtResponse = await this.ec2Client.send(rtCommand);
|
|
680
913
|
|
|
681
914
|
if (rtResponse.RouteTables) {
|
|
682
|
-
resources.routeTables = rtResponse.RouteTables.filter(
|
|
683
|
-
rt
|
|
684
|
-
|
|
685
|
-
|
|
915
|
+
resources.routeTables = rtResponse.RouteTables.filter(
|
|
916
|
+
(rt) =>
|
|
917
|
+
rt.Tags &&
|
|
918
|
+
rt.Tags.some(
|
|
919
|
+
(tag) =>
|
|
920
|
+
tag.Key === 'ManagedBy' && tag.Value === 'Frigg'
|
|
921
|
+
)
|
|
686
922
|
);
|
|
687
923
|
}
|
|
688
924
|
|
|
@@ -693,7 +929,7 @@ class AWSDiscovery {
|
|
|
693
929
|
natGateways: [],
|
|
694
930
|
elasticIps: [],
|
|
695
931
|
subnets: [],
|
|
696
|
-
routeTables: []
|
|
932
|
+
routeTables: [],
|
|
697
933
|
};
|
|
698
934
|
}
|
|
699
935
|
}
|
|
@@ -709,15 +945,15 @@ class AWSDiscovery {
|
|
|
709
945
|
natGatewaysInPrivateSubnets: [],
|
|
710
946
|
orphanedElasticIps: [],
|
|
711
947
|
misconfiguredRouteTables: [],
|
|
712
|
-
privateSubnetsWithoutNatRoute: []
|
|
948
|
+
privateSubnetsWithoutNatRoute: [],
|
|
713
949
|
};
|
|
714
950
|
|
|
715
951
|
// Find NAT Gateways in private subnets
|
|
716
952
|
const natCommand = new DescribeNatGatewaysCommand({
|
|
717
953
|
Filter: [
|
|
718
954
|
{ Name: 'vpc-id', Values: [vpcId] },
|
|
719
|
-
{ Name: 'state', Values: ['available'] }
|
|
720
|
-
]
|
|
955
|
+
{ Name: 'state', Values: ['available'] },
|
|
956
|
+
],
|
|
721
957
|
});
|
|
722
958
|
const natResponse = await this.ec2Client.send(natCommand);
|
|
723
959
|
|
|
@@ -728,7 +964,7 @@ class AWSDiscovery {
|
|
|
728
964
|
misconfigurations.natGatewaysInPrivateSubnets.push({
|
|
729
965
|
natGatewayId: nat.NatGatewayId,
|
|
730
966
|
subnetId: nat.SubnetId,
|
|
731
|
-
tags: nat.Tags
|
|
967
|
+
tags: nat.Tags,
|
|
732
968
|
});
|
|
733
969
|
}
|
|
734
970
|
}
|
|
@@ -740,16 +976,24 @@ class AWSDiscovery {
|
|
|
740
976
|
|
|
741
977
|
if (eipResponse.Addresses) {
|
|
742
978
|
for (const eip of eipResponse.Addresses) {
|
|
743
|
-
if (
|
|
979
|
+
if (
|
|
980
|
+
!eip.InstanceId &&
|
|
981
|
+
!eip.NetworkInterfaceId &&
|
|
982
|
+
!eip.AssociationId
|
|
983
|
+
) {
|
|
744
984
|
// Check if it's Frigg-managed
|
|
745
|
-
const isFriggManaged =
|
|
746
|
-
|
|
747
|
-
|
|
985
|
+
const isFriggManaged =
|
|
986
|
+
eip.Tags &&
|
|
987
|
+
eip.Tags.some(
|
|
988
|
+
(tag) =>
|
|
989
|
+
tag.Key === 'ManagedBy' &&
|
|
990
|
+
tag.Value === 'Frigg'
|
|
991
|
+
);
|
|
748
992
|
if (isFriggManaged) {
|
|
749
993
|
misconfigurations.orphanedElasticIps.push({
|
|
750
994
|
allocationId: eip.AllocationId,
|
|
751
995
|
publicIp: eip.PublicIp,
|
|
752
|
-
tags: eip.Tags
|
|
996
|
+
tags: eip.Tags,
|
|
753
997
|
});
|
|
754
998
|
}
|
|
755
999
|
}
|
|
@@ -765,15 +1009,20 @@ class AWSDiscovery {
|
|
|
765
1009
|
|
|
766
1010
|
// Find route table for this subnet
|
|
767
1011
|
for (const rt of routeTables) {
|
|
768
|
-
const isAssociated =
|
|
769
|
-
|
|
770
|
-
|
|
1012
|
+
const isAssociated =
|
|
1013
|
+
rt.Associations &&
|
|
1014
|
+
rt.Associations.some(
|
|
1015
|
+
(assoc) => assoc.SubnetId === subnet.SubnetId
|
|
1016
|
+
);
|
|
771
1017
|
|
|
772
1018
|
if (isAssociated) {
|
|
773
|
-
hasNatRoute =
|
|
774
|
-
|
|
1019
|
+
hasNatRoute =
|
|
1020
|
+
rt.Routes &&
|
|
1021
|
+
rt.Routes.some(
|
|
1022
|
+
(route) =>
|
|
1023
|
+
route.NatGatewayId &&
|
|
775
1024
|
route.DestinationCidrBlock === '0.0.0.0/0'
|
|
776
|
-
|
|
1025
|
+
);
|
|
777
1026
|
break;
|
|
778
1027
|
}
|
|
779
1028
|
}
|
|
@@ -781,7 +1030,7 @@ class AWSDiscovery {
|
|
|
781
1030
|
if (!hasNatRoute) {
|
|
782
1031
|
misconfigurations.privateSubnetsWithoutNatRoute.push({
|
|
783
1032
|
subnetId: subnet.SubnetId,
|
|
784
|
-
availabilityZone: subnet.AvailabilityZone
|
|
1033
|
+
availabilityZone: subnet.AvailabilityZone,
|
|
785
1034
|
});
|
|
786
1035
|
}
|
|
787
1036
|
}
|
|
@@ -793,7 +1042,7 @@ class AWSDiscovery {
|
|
|
793
1042
|
natGatewaysInPrivateSubnets: [],
|
|
794
1043
|
orphanedElasticIps: [],
|
|
795
1044
|
misconfiguredRouteTables: [],
|
|
796
|
-
privateSubnetsWithoutNatRoute: []
|
|
1045
|
+
privateSubnetsWithoutNatRoute: [],
|
|
797
1046
|
};
|
|
798
1047
|
}
|
|
799
1048
|
}
|
|
@@ -810,8 +1059,12 @@ class AWSDiscovery {
|
|
|
810
1059
|
recommendations.push({
|
|
811
1060
|
severity: 'critical',
|
|
812
1061
|
issue: 'NAT Gateway in private subnet',
|
|
813
|
-
recommendation:
|
|
814
|
-
|
|
1062
|
+
recommendation:
|
|
1063
|
+
'Recreate NAT Gateway in public subnet or fix route tables',
|
|
1064
|
+
affectedResources:
|
|
1065
|
+
misconfigurations.natGatewaysInPrivateSubnets.map(
|
|
1066
|
+
(n) => n.natGatewayId
|
|
1067
|
+
),
|
|
815
1068
|
});
|
|
816
1069
|
}
|
|
817
1070
|
|
|
@@ -820,7 +1073,9 @@ class AWSDiscovery {
|
|
|
820
1073
|
severity: 'warning',
|
|
821
1074
|
issue: 'Orphaned Elastic IPs',
|
|
822
1075
|
recommendation: 'Release unused Elastic IPs to avoid charges',
|
|
823
|
-
affectedResources: misconfigurations.orphanedElasticIps.map(
|
|
1076
|
+
affectedResources: misconfigurations.orphanedElasticIps.map(
|
|
1077
|
+
(e) => e.allocationId
|
|
1078
|
+
),
|
|
824
1079
|
});
|
|
825
1080
|
}
|
|
826
1081
|
|
|
@@ -828,8 +1083,12 @@ class AWSDiscovery {
|
|
|
828
1083
|
recommendations.push({
|
|
829
1084
|
severity: 'critical',
|
|
830
1085
|
issue: 'Private subnets without NAT route',
|
|
831
|
-
recommendation:
|
|
832
|
-
|
|
1086
|
+
recommendation:
|
|
1087
|
+
'Add NAT Gateway route to private subnet route tables',
|
|
1088
|
+
affectedResources:
|
|
1089
|
+
misconfigurations.privateSubnetsWithoutNatRoute.map(
|
|
1090
|
+
(s) => s.subnetId
|
|
1091
|
+
),
|
|
833
1092
|
});
|
|
834
1093
|
}
|
|
835
1094
|
|
|
@@ -856,7 +1115,9 @@ class AWSDiscovery {
|
|
|
856
1115
|
*/
|
|
857
1116
|
async discoverResources(options = {}) {
|
|
858
1117
|
try {
|
|
859
|
-
console.log(
|
|
1118
|
+
console.log(
|
|
1119
|
+
'\nš Discovering AWS resources for Frigg deployment...'
|
|
1120
|
+
);
|
|
860
1121
|
console.log('ā'.repeat(60));
|
|
861
1122
|
|
|
862
1123
|
const vpc = await this.findDefaultVpc();
|
|
@@ -865,17 +1126,30 @@ class AWSDiscovery {
|
|
|
865
1126
|
// Enable auto-convert if selfHeal is enabled
|
|
866
1127
|
const autoConvert = options.selfHeal || false;
|
|
867
1128
|
|
|
868
|
-
const privateSubnets = await this.findPrivateSubnets(
|
|
869
|
-
|
|
1129
|
+
const privateSubnets = await this.findPrivateSubnets(
|
|
1130
|
+
vpc.VpcId,
|
|
1131
|
+
autoConvert
|
|
1132
|
+
);
|
|
1133
|
+
console.log(
|
|
1134
|
+
`\nā
Selected subnets for Lambda: ${privateSubnets
|
|
1135
|
+
.map((s) => s.SubnetId)
|
|
1136
|
+
.join(', ')}`
|
|
1137
|
+
);
|
|
870
1138
|
|
|
871
1139
|
const publicSubnet = await this.findPublicSubnets(vpc.VpcId);
|
|
872
1140
|
if (publicSubnet) {
|
|
873
|
-
console.log(
|
|
1141
|
+
console.log(
|
|
1142
|
+
`\nā
Found public subnet for NAT Gateway: ${publicSubnet.SubnetId}`
|
|
1143
|
+
);
|
|
874
1144
|
} else {
|
|
875
|
-
console.log(
|
|
1145
|
+
console.log(
|
|
1146
|
+
`\nā ļø No public subnet found - NAT Gateway creation may fail`
|
|
1147
|
+
);
|
|
876
1148
|
}
|
|
877
1149
|
|
|
878
|
-
const securityGroup = await this.findDefaultSecurityGroup(
|
|
1150
|
+
const securityGroup = await this.findDefaultSecurityGroup(
|
|
1151
|
+
vpc.VpcId
|
|
1152
|
+
);
|
|
879
1153
|
console.log(`\nā
Found security group: ${securityGroup.GroupId}`);
|
|
880
1154
|
|
|
881
1155
|
const routeTable = await this.findPrivateRouteTable(vpc.VpcId);
|
|
@@ -887,9 +1161,11 @@ class AWSDiscovery {
|
|
|
887
1161
|
} else {
|
|
888
1162
|
console.log('ā¹ļø No KMS key found');
|
|
889
1163
|
}
|
|
890
|
-
|
|
1164
|
+
|
|
891
1165
|
// Try to find existing NAT Gateway
|
|
892
|
-
const existingNatGateway = await this.findExistingNatGateway(
|
|
1166
|
+
const existingNatGateway = await this.findExistingNatGateway(
|
|
1167
|
+
vpc.VpcId
|
|
1168
|
+
);
|
|
893
1169
|
let natGatewayId = null;
|
|
894
1170
|
let elasticIpAllocationId = null;
|
|
895
1171
|
let natGatewayInPrivateSubnet = false;
|
|
@@ -897,11 +1173,16 @@ class AWSDiscovery {
|
|
|
897
1173
|
if (existingNatGateway) {
|
|
898
1174
|
natGatewayId = existingNatGateway.NatGatewayId;
|
|
899
1175
|
// Check if NAT Gateway is in a private subnet (from our detection)
|
|
900
|
-
natGatewayInPrivateSubnet =
|
|
1176
|
+
natGatewayInPrivateSubnet =
|
|
1177
|
+
existingNatGateway._isInPrivateSubnet || false;
|
|
901
1178
|
|
|
902
1179
|
// Get the EIP allocation ID from the NAT Gateway
|
|
903
|
-
if (
|
|
904
|
-
|
|
1180
|
+
if (
|
|
1181
|
+
existingNatGateway.NatGatewayAddresses &&
|
|
1182
|
+
existingNatGateway.NatGatewayAddresses.length > 0
|
|
1183
|
+
) {
|
|
1184
|
+
elasticIpAllocationId =
|
|
1185
|
+
existingNatGateway.NatGatewayAddresses[0].AllocationId;
|
|
905
1186
|
}
|
|
906
1187
|
} else {
|
|
907
1188
|
// If no NAT Gateway exists, check for available EIP
|
|
@@ -912,36 +1193,58 @@ class AWSDiscovery {
|
|
|
912
1193
|
}
|
|
913
1194
|
|
|
914
1195
|
// Check if the "private" subnets are actually public
|
|
915
|
-
const subnet1IsActuallyPrivate = privateSubnets[0]
|
|
916
|
-
await this.isSubnetPrivate(privateSubnets[0].SubnetId)
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
1196
|
+
const subnet1IsActuallyPrivate = privateSubnets[0]
|
|
1197
|
+
? await this.isSubnetPrivate(privateSubnets[0].SubnetId)
|
|
1198
|
+
: false;
|
|
1199
|
+
const subnet2IsActuallyPrivate = privateSubnets[1]
|
|
1200
|
+
? await this.isSubnetPrivate(privateSubnets[1].SubnetId)
|
|
1201
|
+
: subnet1IsActuallyPrivate;
|
|
920
1202
|
|
|
921
1203
|
const subnetStatus = {
|
|
922
|
-
requiresConversion:
|
|
1204
|
+
requiresConversion:
|
|
1205
|
+
!subnet1IsActuallyPrivate || !subnet2IsActuallyPrivate,
|
|
923
1206
|
subnet1NeedsConversion: !subnet1IsActuallyPrivate,
|
|
924
|
-
subnet2NeedsConversion: !subnet2IsActuallyPrivate
|
|
1207
|
+
subnet2NeedsConversion: !subnet2IsActuallyPrivate,
|
|
925
1208
|
};
|
|
926
1209
|
|
|
927
1210
|
if (subnetStatus.requiresConversion) {
|
|
928
1211
|
console.log(`\nā ļø SUBNET CONFIGURATION WARNING:`);
|
|
929
1212
|
if (subnetStatus.subnet1NeedsConversion && privateSubnets[0]) {
|
|
930
|
-
console.log(
|
|
1213
|
+
console.log(
|
|
1214
|
+
` - Subnet ${privateSubnets[0].SubnetId} is currently PUBLIC but will be used for Lambda`
|
|
1215
|
+
);
|
|
931
1216
|
}
|
|
932
1217
|
if (subnetStatus.subnet2NeedsConversion && privateSubnets[1]) {
|
|
933
|
-
console.log(
|
|
1218
|
+
console.log(
|
|
1219
|
+
` - Subnet ${privateSubnets[1].SubnetId} is currently PUBLIC but will be used for Lambda`
|
|
1220
|
+
);
|
|
934
1221
|
}
|
|
935
|
-
console.log(
|
|
1222
|
+
console.log(
|
|
1223
|
+
` š” Enable selfHeal: true to automatically fix this`
|
|
1224
|
+
);
|
|
936
1225
|
}
|
|
937
1226
|
|
|
938
1227
|
console.log(`\n${'ā'.repeat(60)}`);
|
|
939
1228
|
console.log('š Discovery Summary:');
|
|
940
1229
|
console.log(` VPC: ${vpc.VpcId}`);
|
|
941
|
-
console.log(
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
1230
|
+
console.log(
|
|
1231
|
+
` Lambda Subnets: ${privateSubnets
|
|
1232
|
+
.map((s) => s.SubnetId)
|
|
1233
|
+
.join(', ')}`
|
|
1234
|
+
);
|
|
1235
|
+
console.log(
|
|
1236
|
+
` NAT Subnet: ${
|
|
1237
|
+
publicSubnet?.SubnetId || 'None (needs creation)'
|
|
1238
|
+
}`
|
|
1239
|
+
);
|
|
1240
|
+
console.log(
|
|
1241
|
+
` NAT Gateway: ${natGatewayId || 'None (will be created)'}`
|
|
1242
|
+
);
|
|
1243
|
+
console.log(
|
|
1244
|
+
` Elastic IP: ${
|
|
1245
|
+
elasticIpAllocationId || 'None (will be allocated)'
|
|
1246
|
+
}`
|
|
1247
|
+
);
|
|
945
1248
|
if (subnetStatus.requiresConversion) {
|
|
946
1249
|
console.log(` ā ļø Subnet Conversion Required: Yes`);
|
|
947
1250
|
}
|
|
@@ -951,7 +1254,8 @@ class AWSDiscovery {
|
|
|
951
1254
|
defaultVpcId: vpc.VpcId,
|
|
952
1255
|
defaultSecurityGroupId: securityGroup.GroupId,
|
|
953
1256
|
privateSubnetId1: privateSubnets[0]?.SubnetId,
|
|
954
|
-
privateSubnetId2:
|
|
1257
|
+
privateSubnetId2:
|
|
1258
|
+
privateSubnets[1]?.SubnetId || privateSubnets[0]?.SubnetId,
|
|
955
1259
|
publicSubnetId: publicSubnet?.SubnetId || null, // May be null if no public subnet exists
|
|
956
1260
|
privateRouteTableId: routeTable.RouteTableId,
|
|
957
1261
|
defaultKmsKeyId: kmsKeyArn,
|
|
@@ -961,14 +1265,20 @@ class AWSDiscovery {
|
|
|
961
1265
|
subnetConversionRequired: subnetStatus.requiresConversion,
|
|
962
1266
|
privateSubnetsWithWrongRoutes: (() => {
|
|
963
1267
|
const wrongRoutes = [];
|
|
964
|
-
if (
|
|
1268
|
+
if (
|
|
1269
|
+
subnetStatus.subnet1NeedsConversion &&
|
|
1270
|
+
privateSubnets[0]
|
|
1271
|
+
) {
|
|
965
1272
|
wrongRoutes.push(privateSubnets[0].SubnetId);
|
|
966
1273
|
}
|
|
967
|
-
if (
|
|
1274
|
+
if (
|
|
1275
|
+
subnetStatus.subnet2NeedsConversion &&
|
|
1276
|
+
privateSubnets[1]
|
|
1277
|
+
) {
|
|
968
1278
|
wrongRoutes.push(privateSubnets[1].SubnetId);
|
|
969
1279
|
}
|
|
970
1280
|
return wrongRoutes;
|
|
971
|
-
})()
|
|
1281
|
+
})(),
|
|
972
1282
|
};
|
|
973
1283
|
} catch (error) {
|
|
974
1284
|
console.error('Error discovering AWS resources:', error);
|
|
@@ -987,19 +1297,24 @@ class AWSDiscovery {
|
|
|
987
1297
|
Filters: [
|
|
988
1298
|
{
|
|
989
1299
|
Name: 'attachment.vpc-id',
|
|
990
|
-
Values: [vpcId]
|
|
1300
|
+
Values: [vpcId],
|
|
991
1301
|
},
|
|
992
1302
|
{
|
|
993
1303
|
Name: 'attachment.state',
|
|
994
|
-
Values: ['available']
|
|
995
|
-
}
|
|
996
|
-
]
|
|
1304
|
+
Values: ['available'],
|
|
1305
|
+
},
|
|
1306
|
+
],
|
|
997
1307
|
});
|
|
998
1308
|
|
|
999
1309
|
const response = await this.ec2Client.send(command);
|
|
1000
1310
|
|
|
1001
|
-
if (
|
|
1002
|
-
|
|
1311
|
+
if (
|
|
1312
|
+
response.InternetGateways &&
|
|
1313
|
+
response.InternetGateways.length > 0
|
|
1314
|
+
) {
|
|
1315
|
+
console.log(
|
|
1316
|
+
`Found existing Internet Gateway: ${response.InternetGateways[0].InternetGatewayId}`
|
|
1317
|
+
);
|
|
1003
1318
|
return response.InternetGateways[0];
|
|
1004
1319
|
}
|
|
1005
1320
|
|
|
@@ -1023,28 +1338,28 @@ class AWSDiscovery {
|
|
|
1023
1338
|
elasticIps: [],
|
|
1024
1339
|
routeTables: [],
|
|
1025
1340
|
subnets: [],
|
|
1026
|
-
securityGroups: []
|
|
1341
|
+
securityGroups: [],
|
|
1027
1342
|
};
|
|
1028
1343
|
|
|
1029
1344
|
// Common filter for Frigg-managed resources
|
|
1030
1345
|
const friggFilters = [
|
|
1031
1346
|
{
|
|
1032
1347
|
Name: 'tag:ManagedBy',
|
|
1033
|
-
Values: ['Frigg']
|
|
1034
|
-
}
|
|
1348
|
+
Values: ['Frigg'],
|
|
1349
|
+
},
|
|
1035
1350
|
];
|
|
1036
1351
|
|
|
1037
1352
|
if (serviceName) {
|
|
1038
1353
|
friggFilters.push({
|
|
1039
1354
|
Name: 'tag:Service',
|
|
1040
|
-
Values: [serviceName]
|
|
1355
|
+
Values: [serviceName],
|
|
1041
1356
|
});
|
|
1042
1357
|
}
|
|
1043
1358
|
|
|
1044
1359
|
if (stage) {
|
|
1045
1360
|
friggFilters.push({
|
|
1046
1361
|
Name: 'tag:Stage',
|
|
1047
|
-
Values: [stage]
|
|
1362
|
+
Values: [stage],
|
|
1048
1363
|
});
|
|
1049
1364
|
}
|
|
1050
1365
|
|
|
@@ -1055,9 +1370,9 @@ class AWSDiscovery {
|
|
|
1055
1370
|
...friggFilters,
|
|
1056
1371
|
{
|
|
1057
1372
|
Name: 'state',
|
|
1058
|
-
Values: ['available']
|
|
1059
|
-
}
|
|
1060
|
-
]
|
|
1373
|
+
Values: ['available'],
|
|
1374
|
+
},
|
|
1375
|
+
],
|
|
1061
1376
|
});
|
|
1062
1377
|
const natResponse = await this.ec2Client.send(natCommand);
|
|
1063
1378
|
results.natGateways = natResponse.NatGateways || [];
|
|
@@ -1068,7 +1383,7 @@ class AWSDiscovery {
|
|
|
1068
1383
|
// Find Elastic IPs
|
|
1069
1384
|
try {
|
|
1070
1385
|
const eipCommand = new DescribeAddressesCommand({
|
|
1071
|
-
Filters: friggFilters
|
|
1386
|
+
Filters: friggFilters,
|
|
1072
1387
|
});
|
|
1073
1388
|
const eipResponse = await this.ec2Client.send(eipCommand);
|
|
1074
1389
|
results.elasticIps = eipResponse.Addresses || [];
|
|
@@ -1079,7 +1394,7 @@ class AWSDiscovery {
|
|
|
1079
1394
|
// Find Route Tables
|
|
1080
1395
|
try {
|
|
1081
1396
|
const rtCommand = new DescribeRouteTablesCommand({
|
|
1082
|
-
Filters: friggFilters
|
|
1397
|
+
Filters: friggFilters,
|
|
1083
1398
|
});
|
|
1084
1399
|
const rtResponse = await this.ec2Client.send(rtCommand);
|
|
1085
1400
|
results.routeTables = rtResponse.RouteTables || [];
|
|
@@ -1090,7 +1405,7 @@ class AWSDiscovery {
|
|
|
1090
1405
|
// Find Subnets
|
|
1091
1406
|
try {
|
|
1092
1407
|
const subnetCommand = new DescribeSubnetsCommand({
|
|
1093
|
-
Filters: friggFilters
|
|
1408
|
+
Filters: friggFilters,
|
|
1094
1409
|
});
|
|
1095
1410
|
const subnetResponse = await this.ec2Client.send(subnetCommand);
|
|
1096
1411
|
results.subnets = subnetResponse.Subnets || [];
|
|
@@ -1101,12 +1416,15 @@ class AWSDiscovery {
|
|
|
1101
1416
|
// Find Security Groups
|
|
1102
1417
|
try {
|
|
1103
1418
|
const sgCommand = new DescribeSecurityGroupsCommand({
|
|
1104
|
-
Filters: friggFilters
|
|
1419
|
+
Filters: friggFilters,
|
|
1105
1420
|
});
|
|
1106
1421
|
const sgResponse = await this.ec2Client.send(sgCommand);
|
|
1107
1422
|
results.securityGroups = sgResponse.SecurityGroups || [];
|
|
1108
1423
|
} catch (err) {
|
|
1109
|
-
console.warn(
|
|
1424
|
+
console.warn(
|
|
1425
|
+
'Error finding Frigg Security Groups:',
|
|
1426
|
+
err.message
|
|
1427
|
+
);
|
|
1110
1428
|
}
|
|
1111
1429
|
|
|
1112
1430
|
console.log('Found Frigg-managed resources:', {
|
|
@@ -1114,7 +1432,7 @@ class AWSDiscovery {
|
|
|
1114
1432
|
elasticIps: results.elasticIps.length,
|
|
1115
1433
|
routeTables: results.routeTables.length,
|
|
1116
1434
|
subnets: results.subnets.length,
|
|
1117
|
-
securityGroups: results.securityGroups.length
|
|
1435
|
+
securityGroups: results.securityGroups.length,
|
|
1118
1436
|
});
|
|
1119
1437
|
|
|
1120
1438
|
return results;
|
|
@@ -1125,10 +1443,10 @@ class AWSDiscovery {
|
|
|
1125
1443
|
elasticIps: [],
|
|
1126
1444
|
routeTables: [],
|
|
1127
1445
|
subnets: [],
|
|
1128
|
-
securityGroups: []
|
|
1446
|
+
securityGroups: [],
|
|
1129
1447
|
};
|
|
1130
1448
|
}
|
|
1131
1449
|
}
|
|
1132
1450
|
}
|
|
1133
1451
|
|
|
1134
|
-
module.exports = { AWSDiscovery };
|
|
1452
|
+
module.exports = { AWSDiscovery };
|