@friggframework/devtools 2.0.0--canary.461.ec909cf.0 → 2.0.0--canary.461.9483dbe.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.
Files changed (71) hide show
  1. package/frigg-cli/__tests__/unit/commands/build.test.js +6 -6
  2. package/frigg-cli/build-command/index.js +1 -1
  3. package/frigg-cli/deploy-command/index.js +6 -6
  4. package/frigg-cli/generate-command/index.js +2 -2
  5. package/frigg-cli/generate-iam-command.js +10 -10
  6. package/frigg-cli/start-command/index.js +1 -1
  7. package/frigg-cli/start-command/start-command.test.js +3 -3
  8. package/frigg-cli/utils/database-validator.js +14 -21
  9. package/infrastructure/REFACTOR.md +532 -0
  10. package/infrastructure/TRANSFORMATION-VISUAL.md +239 -0
  11. package/infrastructure/__tests__/postgres-config.test.js +1 -1
  12. package/infrastructure/create-frigg-infrastructure.js +1 -1
  13. package/infrastructure/{DEPLOYMENT-INSTRUCTIONS.md → docs/deployment-instructions.md} +3 -3
  14. package/infrastructure/{IAM-POLICY-TEMPLATES.md → docs/iam-policy-templates.md} +9 -10
  15. package/infrastructure/domains/database/aurora-discovery.js +81 -0
  16. package/infrastructure/domains/database/aurora-discovery.test.js +188 -0
  17. package/infrastructure/domains/integration/integration-builder.js +178 -0
  18. package/infrastructure/domains/integration/integration-builder.test.js +362 -0
  19. package/infrastructure/domains/integration/websocket-builder.js +69 -0
  20. package/infrastructure/domains/integration/websocket-builder.test.js +195 -0
  21. package/infrastructure/domains/networking/vpc-discovery.test.js +257 -0
  22. package/infrastructure/domains/parameters/ssm-builder.js +79 -0
  23. package/infrastructure/domains/parameters/ssm-builder.test.js +188 -0
  24. package/infrastructure/domains/parameters/ssm-discovery.js +84 -0
  25. package/infrastructure/domains/parameters/ssm-discovery.test.js +210 -0
  26. package/infrastructure/{iam-generator.js → domains/security/iam-generator.js} +2 -2
  27. package/infrastructure/domains/security/kms-builder.js +169 -0
  28. package/infrastructure/domains/security/kms-builder.test.js +354 -0
  29. package/infrastructure/domains/security/kms-discovery.js +80 -0
  30. package/infrastructure/domains/security/kms-discovery.test.js +176 -0
  31. package/infrastructure/domains/shared/base-builder.js +112 -0
  32. package/infrastructure/domains/shared/builder-orchestrator.js +212 -0
  33. package/infrastructure/domains/shared/builder-orchestrator.test.js +213 -0
  34. package/infrastructure/domains/shared/environment-builder.js +118 -0
  35. package/infrastructure/domains/shared/environment-builder.test.js +246 -0
  36. package/infrastructure/domains/shared/providers/aws-provider-adapter.test.js +366 -0
  37. package/infrastructure/domains/shared/providers/azure-provider-adapter.stub.js +93 -0
  38. package/infrastructure/domains/shared/providers/cloud-provider-adapter.js +136 -0
  39. package/infrastructure/domains/shared/providers/gcp-provider-adapter.stub.js +82 -0
  40. package/infrastructure/domains/shared/providers/provider-factory.js +108 -0
  41. package/infrastructure/domains/shared/providers/provider-factory.test.js +170 -0
  42. package/infrastructure/domains/shared/resource-discovery.js +132 -0
  43. package/infrastructure/domains/shared/resource-discovery.test.js +410 -0
  44. package/infrastructure/domains/shared/utilities/base-definition-factory.js +2 -3
  45. package/infrastructure/domains/shared/utilities/base-definition-factory.js.bak +338 -0
  46. package/infrastructure/domains/shared/utilities/base-definition-factory.test.js +248 -0
  47. package/infrastructure/domains/shared/utilities/handler-path-resolver.test.js +259 -0
  48. package/infrastructure/domains/shared/utilities/prisma-layer-manager.js +55 -0
  49. package/infrastructure/domains/shared/utilities/prisma-layer-manager.test.js +134 -0
  50. package/infrastructure/domains/shared/validation/env-validator.test.js +173 -0
  51. package/infrastructure/esbuild.config.js +53 -0
  52. package/infrastructure/infrastructure-composer.js +85 -0
  53. package/infrastructure/scripts/build-prisma-layer.js +60 -47
  54. package/infrastructure/{build-time-discovery.test.js → scripts/build-time-discovery.test.js} +5 -4
  55. package/layers/prisma/nodejs/package.json +8 -0
  56. package/management-ui/server/utils/environment/awsParameterStore.js +29 -18
  57. package/package.json +8 -8
  58. package/infrastructure/aws-discovery.js +0 -1704
  59. package/infrastructure/aws-discovery.test.js +0 -1666
  60. package/infrastructure/serverless-template.js +0 -2804
  61. package/infrastructure/serverless-template.test.js +0 -1897
  62. /package/infrastructure/{POSTGRES-CONFIGURATION.md → docs/POSTGRES-CONFIGURATION.md} +0 -0
  63. /package/infrastructure/{WEBSOCKET-CONFIGURATION.md → docs/WEBSOCKET-CONFIGURATION.md} +0 -0
  64. /package/infrastructure/{GENERATE-IAM-DOCS.md → docs/generate-iam-command.md} +0 -0
  65. /package/infrastructure/{iam-generator.test.js → domains/security/iam-generator.test.js} +0 -0
  66. /package/infrastructure/{frigg-deployment-iam-stack.yaml → domains/security/templates/frigg-deployment-iam-stack.yaml} +0 -0
  67. /package/infrastructure/{iam-policy-basic.json → domains/security/templates/iam-policy-basic.json} +0 -0
  68. /package/infrastructure/{iam-policy-full.json → domains/security/templates/iam-policy-full.json} +0 -0
  69. /package/infrastructure/{env-validator.js → domains/shared/validation/env-validator.js} +0 -0
  70. /package/infrastructure/{build-time-discovery.js → scripts/build-time-discovery.js} +0 -0
  71. /package/infrastructure/{run-discovery.js → scripts/run-discovery.js} +0 -0
@@ -1,1704 +0,0 @@
1
- let EC2Client,
2
- DescribeVpcsCommand,
3
- DescribeSubnetsCommand,
4
- DescribeSecurityGroupsCommand,
5
- DescribeRouteTablesCommand,
6
- DescribeNatGatewaysCommand,
7
- DescribeAddressesCommand,
8
- DescribeInternetGatewaysCommand;
9
- let KMSClient, ListKeysCommand, DescribeKeyCommand;
10
- let STSClient, GetCallerIdentityCommand;
11
- let RDSClient, DescribeDBClustersCommand, DescribeDBSubnetGroupsCommand;
12
- let SecretsManagerClient, ListSecretsCommand, DescribeSecretCommand;
13
-
14
- function loadEC2() {
15
- if (!EC2Client) {
16
- ({
17
- EC2Client,
18
- DescribeVpcsCommand,
19
- DescribeSubnetsCommand,
20
- DescribeSecurityGroupsCommand,
21
- DescribeRouteTablesCommand,
22
- DescribeNatGatewaysCommand,
23
- DescribeAddressesCommand,
24
- DescribeInternetGatewaysCommand,
25
- } = require('@aws-sdk/client-ec2'));
26
- }
27
- }
28
-
29
- function loadKMS() {
30
- if (!KMSClient) {
31
- ({
32
- KMSClient,
33
- ListKeysCommand,
34
- DescribeKeyCommand,
35
- ListAliasesCommand,
36
- } = require('@aws-sdk/client-kms'));
37
- }
38
- }
39
-
40
- function loadSTS() {
41
- if (!STSClient) {
42
- ({
43
- STSClient,
44
- GetCallerIdentityCommand,
45
- } = require('@aws-sdk/client-sts'));
46
- }
47
- }
48
-
49
- function loadRDS() {
50
- if (!RDSClient) {
51
- ({
52
- RDSClient,
53
- DescribeDBClustersCommand,
54
- DescribeDBSubnetGroupsCommand,
55
- } = require('@aws-sdk/client-rds'));
56
- }
57
- }
58
-
59
- function loadSecretsManager() {
60
- if (!SecretsManagerClient) {
61
- ({
62
- SecretsManagerClient,
63
- ListSecretsCommand,
64
- DescribeSecretCommand,
65
- } = require('@aws-sdk/client-secrets-manager'));
66
- }
67
- }
68
-
69
- class AWSDiscovery {
70
- constructor(region = 'us-east-1') {
71
- console.log('[AWSDiscovery] Initializing AWSDiscovery...');
72
- console.log('[AWSDiscovery] Region:', region);
73
- console.log('[AWSDiscovery] System time:', new Date().toISOString());
74
- console.log('[AWSDiscovery] AWS_PROFILE:', process.env.AWS_PROFILE);
75
- console.log('[AWSDiscovery] AWS_ACCESS_KEY_ID:', process.env.AWS_ACCESS_KEY_ID ? 'SET (hidden)' : 'NOT SET');
76
- console.log('[AWSDiscovery] AWS_SECRET_ACCESS_KEY:', process.env.AWS_SECRET_ACCESS_KEY ? 'SET (hidden)' : 'NOT SET');
77
- console.log('[AWSDiscovery] NODE_TLS_REJECT_UNAUTHORIZED:', process.env.NODE_TLS_REJECT_UNAUTHORIZED);
78
-
79
- this.region = region;
80
- loadEC2();
81
- loadKMS();
82
- loadSTS();
83
- loadRDS();
84
- loadSecretsManager();
85
- this.ec2Client = new EC2Client({ region });
86
- this.kmsClient = new KMSClient({ region });
87
- this.stsClient = new STSClient({ region });
88
- this.rdsClient = new RDSClient({ region });
89
- this.secretsManagerClient = new SecretsManagerClient({ region });
90
-
91
- console.log('[AWSDiscovery] AWS clients initialized successfully');
92
- }
93
-
94
- async validateCredentials() {
95
- console.log('[AWSDiscovery] Validating AWS credentials...');
96
-
97
- try {
98
- const command = new GetCallerIdentityCommand({});
99
- const startTime = Date.now();
100
- const response = await this.stsClient.send(command);
101
- const duration = Date.now() - startTime;
102
-
103
- console.log('[AWSDiscovery] ✅ Credentials are VALID');
104
- console.log('[AWSDiscovery] Account ID:', response.Account);
105
- console.log('[AWSDiscovery] User ARN:', response.Arn);
106
- console.log('[AWSDiscovery] User ID:', response.UserId);
107
- console.log('[AWSDiscovery] Validation took', duration, 'ms');
108
-
109
- return {
110
- valid: true,
111
- accountId: response.Account,
112
- arn: response.Arn,
113
- userId: response.UserId
114
- };
115
- } catch (error) {
116
- console.error('[AWSDiscovery] ❌ CREDENTIAL VALIDATION FAILED');
117
- console.error('[AWSDiscovery] Error:', error.message);
118
- console.error('[AWSDiscovery] Error Code:', error.Code || error.code);
119
-
120
- // Provide specific guidance based on error type
121
- if (error.Code === 'RequestExpired' || error.message.includes('expired')) {
122
- console.error('\n[AWSDiscovery] 🔍 DIAGNOSIS: Expired Credentials');
123
- console.error('[AWSDiscovery] Your AWS credentials have expired.');
124
- console.error('[AWSDiscovery] This commonly happens with:');
125
- console.error('[AWSDiscovery] - Temporary STS credentials (AWS_ACCESS_KEY_ID starting with "ASIA")');
126
- console.error('[AWSDiscovery] - AWS SSO sessions that have timed out');
127
- console.error('[AWSDiscovery] - Hardcoded credentials in .env files');
128
- console.error('\n[AWSDiscovery] 💡 SOLUTIONS:');
129
- if (process.env.AWS_ACCESS_KEY_ID?.startsWith('ASIA')) {
130
- console.error('[AWSDiscovery] 1. Comment out AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY in your .env file');
131
- console.error('[AWSDiscovery] 2. Use AWS_PROFILE instead: AWS_PROFILE=your-profile npm run deploy');
132
- } else if (process.env.AWS_PROFILE) {
133
- console.error('[AWSDiscovery] 1. Refresh your AWS SSO login: aws sso login --profile', process.env.AWS_PROFILE);
134
- console.error('[AWSDiscovery] 2. Or regenerate credentials if using IAM user');
135
- } else {
136
- console.error('[AWSDiscovery] 1. Set up AWS profile: aws configure --profile your-profile');
137
- console.error('[AWSDiscovery] 2. Or use AWS SSO: aws sso login');
138
- }
139
- } else if (error.Code === 'InvalidClientTokenId' || error.Code === 'SignatureDoesNotMatch') {
140
- console.error('\n[AWSDiscovery] 🔍 DIAGNOSIS: Invalid Credentials');
141
- console.error('[AWSDiscovery] Your AWS credentials are not recognized or incorrect.');
142
- console.error('\n[AWSDiscovery] 💡 SOLUTIONS:');
143
- console.error('[AWSDiscovery] 1. Check AWS credentials file: cat ~/.aws/credentials');
144
- console.error('[AWSDiscovery] 2. Verify profile exists: aws configure list-profiles');
145
- console.error('[AWSDiscovery] 3. Test credentials: aws sts get-caller-identity --profile', process.env.AWS_PROFILE || 'default');
146
- } else if (error.message.includes('Could not load credentials')) {
147
- console.error('\n[AWSDiscovery] 🔍 DIAGNOSIS: No Credentials Found');
148
- console.error('[AWSDiscovery] AWS SDK cannot find any credentials.');
149
- console.error('\n[AWSDiscovery] 💡 SOLUTIONS:');
150
- console.error('[AWSDiscovery] 1. Set AWS_PROFILE: export AWS_PROFILE=your-profile');
151
- console.error('[AWSDiscovery] 2. Or configure default profile: aws configure');
152
- console.error('[AWSDiscovery] 3. Or use AWS SSO: aws sso login');
153
- } else {
154
- console.error('\n[AWSDiscovery] 💡 GENERAL TROUBLESHOOTING:');
155
- console.error('[AWSDiscovery] 1. Test credentials manually: aws sts get-caller-identity');
156
- console.error('[AWSDiscovery] 2. Check ~/.aws/credentials file exists');
157
- console.error('[AWSDiscovery] 3. Verify network connectivity to AWS');
158
- }
159
-
160
- console.error('\n[AWSDiscovery] ⛔ Cannot proceed with AWS discovery until credentials are valid.\n');
161
-
162
- throw new Error(`AWS credential validation failed: ${error.message}. See detailed guidance above.`);
163
- }
164
- }
165
-
166
- async getAccountId() {
167
- try {
168
- const command = new GetCallerIdentityCommand({});
169
- const response = await this.stsClient.send(command);
170
- return response.Account;
171
- } catch (error) {
172
- console.error('Error getting AWS account ID:', error.message);
173
- throw error;
174
- }
175
- }
176
-
177
- async findDefaultVpc() {
178
- try {
179
- console.log('[AWSDiscovery.findDefaultVpc] Starting VPC discovery...');
180
- console.log('[AWSDiscovery.findDefaultVpc] Request timestamp:', new Date().toISOString());
181
- console.log('[AWSDiscovery.findDefaultVpc] Region:', this.region);
182
-
183
- const command = new DescribeVpcsCommand({
184
- Filters: [
185
- {
186
- Name: 'is-default',
187
- Values: ['true'],
188
- },
189
- ],
190
- });
191
-
192
- console.log('[AWSDiscovery.findDefaultVpc] Sending DescribeVpcsCommand (default VPC)...');
193
- console.log('[AWSDiscovery.findDefaultVpc] Request time before send:', new Date().toISOString());
194
-
195
- const requestStart = Date.now();
196
- const response = await this.ec2Client.send(command);
197
- const requestDuration = Date.now() - requestStart;
198
-
199
- console.log('[AWSDiscovery.findDefaultVpc] Request completed in', requestDuration, 'ms');
200
- console.log('[AWSDiscovery.findDefaultVpc] Response time:', new Date().toISOString());
201
- console.log('[AWSDiscovery.findDefaultVpc] Found', response.Vpcs?.length || 0, 'default VPC(s)');
202
-
203
- if (response.Vpcs && response.Vpcs.length > 0) {
204
- console.log('[AWSDiscovery.findDefaultVpc] Using default VPC:', response.Vpcs[0].VpcId);
205
- return response.Vpcs[0];
206
- }
207
-
208
- console.log('[AWSDiscovery.findDefaultVpc] No default VPC found, fetching all VPCs...');
209
- const allVpcsCommand = new DescribeVpcsCommand({});
210
-
211
- console.log('[AWSDiscovery.findDefaultVpc] Sending DescribeVpcsCommand (all VPCs)...');
212
- const allVpcsRequestStart = Date.now();
213
- const allVpcsResponse = await this.ec2Client.send(allVpcsCommand);
214
- const allVpcsRequestDuration = Date.now() - allVpcsRequestStart;
215
-
216
- console.log('[AWSDiscovery.findDefaultVpc] All VPCs request completed in', allVpcsRequestDuration, 'ms');
217
- console.log('[AWSDiscovery.findDefaultVpc] Found', allVpcsResponse.Vpcs?.length || 0, 'VPC(s)');
218
-
219
- if (allVpcsResponse.Vpcs && allVpcsResponse.Vpcs.length > 0) {
220
- console.log('No default VPC found, using first available VPC');
221
- console.log('[AWSDiscovery.findDefaultVpc] Using VPC:', allVpcsResponse.Vpcs[0].VpcId);
222
- return allVpcsResponse.Vpcs[0];
223
- }
224
-
225
- throw new Error('No VPC found in the account');
226
- } catch (error) {
227
- console.error('[AWSDiscovery.findDefaultVpc] ERROR occurred at:', new Date().toISOString());
228
- console.error('[AWSDiscovery.findDefaultVpc] Error type:', error.constructor.name);
229
- console.error('[AWSDiscovery.findDefaultVpc] Error code:', error.Code || error.code);
230
- console.error('[AWSDiscovery.findDefaultVpc] Error message:', error.message);
231
- console.error('[AWSDiscovery.findDefaultVpc] Error $fault:', error.$fault);
232
- console.error('[AWSDiscovery.findDefaultVpc] Error $metadata:', JSON.stringify(error.$metadata, null, 2));
233
-
234
- if (error.$response) {
235
- console.error('[AWSDiscovery.findDefaultVpc] Response status:', error.$response.statusCode);
236
- console.error('[AWSDiscovery.findDefaultVpc] Response headers:', JSON.stringify(error.$response.headers, null, 2));
237
- }
238
-
239
- console.error('[AWSDiscovery.findDefaultVpc] Full error object:', JSON.stringify(error, Object.getOwnPropertyNames(error), 2));
240
- throw error;
241
- }
242
- }
243
-
244
- async findPrivateSubnets(vpcId, autoConvert = false) {
245
- try {
246
- const subnets = await this._fetchSubnets(vpcId);
247
-
248
- if (subnets.length === 0) {
249
- throw new Error(`No subnets found in VPC ${vpcId}`);
250
- }
251
-
252
- console.log(`\n🔍 Analyzing ${subnets.length} subnets in VPC ${vpcId}...`);
253
-
254
- const { privateSubnets, publicSubnets } = await this._classifySubnets(
255
- subnets,
256
- { logDetails: true }
257
- );
258
-
259
- this._logSubnetSummary(privateSubnets.length, publicSubnets.length);
260
-
261
- const selection = this._selectSubnetsForLambda({
262
- privateSubnets,
263
- publicSubnets,
264
- autoConvert,
265
- vpcId,
266
- });
267
-
268
- if (selection) {
269
- return selection;
270
- }
271
-
272
- throw new Error(`No subnets found in VPC ${vpcId}`);
273
- } catch (error) {
274
- console.error('Error finding private subnets:', error);
275
- throw error;
276
- }
277
- }
278
-
279
- async isSubnetPublic(subnetId, vpcId) {
280
- const isPrivate = await this.isSubnetPrivate(subnetId, vpcId);
281
- return !isPrivate;
282
- }
283
-
284
- async isSubnetPrivate(subnetId, vpcId) {
285
- try {
286
- const targetVpcId = vpcId || (await this._getSubnetVpcId(subnetId));
287
-
288
- const routeTables = await this.findRouteTables(targetVpcId);
289
- const routeTable = this._findRouteTableForSubnet(routeTables, subnetId);
290
-
291
- if (!routeTable) {
292
- console.warn(`No route table found for subnet ${subnetId}`);
293
- return true;
294
- }
295
-
296
- const gatewayId = this._findIgwRoute(routeTable);
297
- if (gatewayId) {
298
- console.log(
299
- `✅ Subnet ${subnetId} is PUBLIC (has route to IGW ${gatewayId})`
300
- );
301
- return false;
302
- }
303
-
304
- console.log(
305
- `🔒 Subnet ${subnetId} is PRIVATE (no IGW route found)`
306
- );
307
- return true;
308
- } catch (error) {
309
- console.warn(
310
- `Could not determine if subnet ${subnetId} is private:`,
311
- error
312
- );
313
- return true;
314
- }
315
- }
316
-
317
- async findDefaultSecurityGroup(vpcId) {
318
- try {
319
- const friggGroup = await this._findSecurityGroupByName(
320
- vpcId,
321
- 'frigg-lambda-sg'
322
- );
323
- if (friggGroup) {
324
- return friggGroup;
325
- }
326
-
327
- const defaultGroup = await this._findSecurityGroupByName(vpcId, 'default');
328
- if (defaultGroup) {
329
- return defaultGroup;
330
- }
331
-
332
- throw new Error(`No security group found for VPC ${vpcId}`);
333
- } catch (error) {
334
- console.error('Error finding default security group:', error);
335
- throw error;
336
- }
337
- }
338
-
339
- async findPublicSubnets(vpcId) {
340
- try {
341
- const subnets = await this._fetchSubnets(vpcId);
342
-
343
- if (subnets.length === 0) {
344
- throw new Error(`No subnets found in VPC ${vpcId}`);
345
- }
346
-
347
- const { publicSubnets } = await this._classifySubnets(subnets);
348
-
349
- if (publicSubnets.length === 0) {
350
- console.warn(
351
- `WARNING: No public subnets found in VPC ${vpcId}`
352
- );
353
- console.warn(
354
- 'A public subnet with Internet Gateway route is required for NAT Gateway placement'
355
- );
356
- console.warn(
357
- 'Please create a public subnet or use VPC endpoints instead'
358
- );
359
- return { primary: null, secondary: null, all: [] };
360
- }
361
-
362
- // Sort by AZ to get subnets in different zones
363
- const sortedByAz = publicSubnets.sort((a, b) =>
364
- a.AvailabilityZone.localeCompare(b.AvailabilityZone)
365
- );
366
-
367
- // Get subnets from different AZs if possible
368
- const primary = sortedByAz[0];
369
- const secondary = sortedByAz.find(s => s.AvailabilityZone !== primary.AvailabilityZone)
370
- || sortedByAz[1]
371
- || sortedByAz[0]; // Fallback to same subnet if only one exists
372
-
373
- console.log(
374
- `Found ${publicSubnets.length} public subnet(s)`
375
- );
376
- console.log(` Primary (NAT Gateway): ${primary.SubnetId} (${primary.AvailabilityZone})`);
377
- if (secondary.SubnetId !== primary.SubnetId) {
378
- console.log(` Secondary (Aurora): ${secondary.SubnetId} (${secondary.AvailabilityZone})`);
379
- } else {
380
- console.warn(` ⚠️ Only one public subnet found - Aurora public deployments will require creating a second subnet`);
381
- }
382
-
383
- return { primary, secondary, all: publicSubnets };
384
- } catch (error) {
385
- console.error('Error finding public subnets:', error);
386
- throw error;
387
- }
388
- }
389
-
390
- async findPrivateRouteTable(vpcId) {
391
- try {
392
- const routeTables = await this.findRouteTables(vpcId);
393
-
394
- if (routeTables.length === 0) {
395
- throw new Error(`No route tables found for VPC ${vpcId}`);
396
- }
397
-
398
- const privateTable = routeTables.find(
399
- (rt) => !this._findIgwRoute(rt)
400
- );
401
-
402
- return privateTable || routeTables[0];
403
- } catch (error) {
404
- console.error('Error finding private route table:', error);
405
- throw error;
406
- }
407
- }
408
-
409
- async findExistingNatGateway(vpcId) {
410
- try {
411
- const command = new DescribeNatGatewaysCommand({
412
- Filter: [
413
- {
414
- Name: 'vpc-id',
415
- Values: [vpcId],
416
- },
417
- {
418
- Name: 'state',
419
- Values: ['available'],
420
- },
421
- ],
422
- });
423
-
424
- const response = await this.ec2Client.send(command);
425
-
426
- const natGateways = (response.NatGateways || []).filter((nat) => {
427
- if (nat.State !== 'available') {
428
- console.warn(
429
- `Skipping NAT Gateway ${nat.NatGatewayId} with state: ${nat.State}`
430
- );
431
- return false;
432
- }
433
- return true;
434
- });
435
-
436
- if (natGateways.length === 0) {
437
- console.warn('No truly available NAT Gateways found in VPC');
438
- return null;
439
- }
440
-
441
- const sortedNatGateways = natGateways.sort((a, b) => {
442
- const aIsFrigg = this._isFriggManaged(a.Tags);
443
- const bIsFrigg = this._isFriggManaged(b.Tags);
444
-
445
- if (aIsFrigg && !bIsFrigg) return -1;
446
- if (!aIsFrigg && bIsFrigg) return 1;
447
- return 0;
448
- });
449
-
450
- for (const natGateway of sortedNatGateways) {
451
- const subnetId = natGateway.SubnetId;
452
- const isPrivate = await this.isSubnetPrivate(
453
- subnetId,
454
- natGateway.VpcId
455
- );
456
- const isFriggNat = this._isFriggManaged(natGateway.Tags);
457
-
458
- if (isPrivate) {
459
- console.warn(
460
- `WARNING: NAT Gateway ${natGateway.NatGatewayId} is in subnet ${subnetId} which appears to be private`
461
- );
462
-
463
- if (isFriggNat) {
464
- console.warn(
465
- 'This is a Frigg-managed NAT Gateway that may have been misconfigured by route table changes'
466
- );
467
- console.warn(
468
- 'Consider enabling selfHeal: true to fix this automatically'
469
- );
470
- natGateway._isInPrivateSubnet = true;
471
- return natGateway;
472
- }
473
-
474
- console.warn(
475
- 'NAT Gateways MUST be placed in public subnets with Internet Gateway routes'
476
- );
477
- console.warn('Skipping this misconfigured NAT Gateway...');
478
- continue;
479
- }
480
-
481
- if (isFriggNat) {
482
- console.log(
483
- `Found existing Frigg-managed NAT Gateway: ${natGateway.NatGatewayId} (State: ${natGateway.State})`
484
- );
485
- natGateway._isInPrivateSubnet = false;
486
- return natGateway;
487
- }
488
-
489
- console.log(
490
- `Found existing NAT Gateway in public subnet: ${natGateway.NatGatewayId} (State: ${natGateway.State})`
491
- );
492
- natGateway._isInPrivateSubnet = false;
493
- return natGateway;
494
- }
495
-
496
- console.error(
497
- `ERROR: Found ${(response.NatGateways || []).length} NAT Gateway(s) but all non-Frigg ones are in private subnets!`
498
- );
499
- console.error(
500
- 'These NAT Gateways will not provide internet connectivity without route table fixes'
501
- );
502
- console.error(
503
- 'Enable selfHeal: true to fix automatically or create a new NAT Gateway'
504
- );
505
- return null;
506
- } catch (error) {
507
- console.warn('Error finding existing NAT Gateway:', error.message);
508
- return null;
509
- }
510
- }
511
-
512
- async findAvailableElasticIP() {
513
- try {
514
- const command = new DescribeAddressesCommand({});
515
- const response = await this.ec2Client.send(command);
516
-
517
- if (response.Addresses && response.Addresses.length > 0) {
518
- const availableEIP = response.Addresses.find(
519
- (eip) =>
520
- !eip.AssociationId &&
521
- !eip.InstanceId &&
522
- !eip.NetworkInterfaceId
523
- );
524
-
525
- if (availableEIP) {
526
- console.log(
527
- `Found available Elastic IP: ${availableEIP.AllocationId}`
528
- );
529
- return availableEIP;
530
- }
531
-
532
- const friggEIP = response.Addresses.find((eip) =>
533
- this._isFriggManaged(eip.Tags)
534
- );
535
-
536
- if (friggEIP) {
537
- console.log(
538
- `Found Frigg-tagged Elastic IP: ${friggEIP.AllocationId}`
539
- );
540
- return friggEIP;
541
- }
542
- }
543
-
544
- return null;
545
- } catch (error) {
546
- console.warn('Error finding available Elastic IP:', error.message);
547
- return null;
548
- }
549
- }
550
-
551
- async findDefaultKmsKey() {
552
- console.log('KMS Discovery Starting...');
553
- try {
554
- console.log(`[KMS Discovery] Running in region: ${this.region}`);
555
- try {
556
- const accountId = await this.getAccountId();
557
- console.log(`[KMS Discovery] AWS Account ID: ${accountId}`);
558
- } catch (error) {
559
- console.warn(
560
- '[KMS Discovery] Could not retrieve account ID:',
561
- error.message
562
- );
563
- }
564
-
565
- const command = new ListKeysCommand({});
566
- const response = await this.kmsClient.send(command);
567
-
568
- if (!response.Keys || response.Keys.length === 0) {
569
- console.log('[KMS Discovery] No KMS keys found in account');
570
- return null;
571
- }
572
-
573
- console.log(
574
- `[KMS Discovery] Found ${response.Keys.length} total keys in account`
575
- );
576
- let keysExamined = 0;
577
- let customerManagedKeys = 0;
578
- let enabledKeys = 0;
579
- let pendingDeletionKeys = 0;
580
-
581
- for (const key of response.Keys) {
582
- try {
583
- const describeCommand = new DescribeKeyCommand({
584
- KeyId: key.KeyId,
585
- });
586
- const keyDetails = await this.kmsClient.send(
587
- describeCommand
588
- );
589
- keysExamined++;
590
-
591
- const metadata = keyDetails.KeyMetadata;
592
- if (!metadata) {
593
- continue;
594
- }
595
-
596
- console.log(`[KMS Discovery] Key ${key.KeyId}:`, {
597
- KeyManager: metadata.KeyManager,
598
- KeyState: metadata.KeyState,
599
- Enabled: metadata.Enabled,
600
- DeletionDate:
601
- metadata.DeletionDate || 'Not scheduled for deletion',
602
- Arn: metadata.Arn,
603
- });
604
-
605
- if (metadata.KeyManager === 'CUSTOMER') {
606
- customerManagedKeys++;
607
-
608
- if (metadata.KeyState === 'Enabled') {
609
- enabledKeys++;
610
- } else if (metadata.KeyState === 'PendingDeletion') {
611
- pendingDeletionKeys++;
612
- console.warn(
613
- `[KMS Discovery] Skipping key ${key.KeyId} - State: PendingDeletion, DeletionDate: ${metadata.DeletionDate}`
614
- );
615
- }
616
-
617
- if (
618
- metadata.KeyState === 'Enabled' &&
619
- !metadata.DeletionDate
620
- ) {
621
- console.log(
622
- `[KMS Discovery] Found eligible customer managed KMS key: ${metadata.Arn}`
623
- );
624
- return metadata.Arn;
625
- } else if (
626
- metadata.KeyState === 'Enabled' &&
627
- metadata.DeletionDate
628
- ) {
629
- console.error(
630
- `[KMS Discovery] WARNING: Key ${key.KeyId} has KeyState='Enabled' but DeletionDate is set: ${metadata.DeletionDate}`
631
- );
632
- }
633
- }
634
- } catch (error) {
635
- console.warn(
636
- `[KMS Discovery] Could not describe key ${key.KeyId}:`,
637
- error.message
638
- );
639
- continue;
640
- }
641
- }
642
-
643
- console.log('[KMS Discovery] Summary:', {
644
- totalKeys: response.Keys.length,
645
- keysExamined,
646
- customerManagedKeys,
647
- enabledKeys,
648
- pendingDeletionKeys,
649
- });
650
-
651
- if (customerManagedKeys === 0) {
652
- console.log(
653
- '[KMS Discovery] No customer managed KMS keys found in account'
654
- );
655
- } else if (enabledKeys === 0) {
656
- console.warn(
657
- '[KMS Discovery] Found customer managed keys but none are in Enabled state'
658
- );
659
- } else {
660
- console.warn(
661
- '[KMS Discovery] Found enabled customer managed keys but none met all criteria'
662
- );
663
- }
664
-
665
- return null;
666
- } catch (error) {
667
- console.error(
668
- '[KMS Discovery] Error finding default KMS key:',
669
- error
670
- );
671
- return null;
672
- }
673
- }
674
-
675
- async findKmsAlias(aliasName) {
676
- try {
677
- console.log(`[KMS Alias Discovery] Checking for alias: ${aliasName}`);
678
- const command = new ListAliasesCommand({});
679
- const response = await this.kmsClient.send(command);
680
-
681
- if (!response.Aliases || response.Aliases.length === 0) {
682
- console.log('[KMS Alias Discovery] No aliases found in account');
683
- return null;
684
- }
685
-
686
- const targetAlias = response.Aliases.find(
687
- alias => alias.AliasName === aliasName
688
- );
689
-
690
- if (targetAlias) {
691
- console.log(`[KMS Alias Discovery] ✅ Found existing alias: ${aliasName}`);
692
- console.log(`[KMS Alias Discovery] Target Key: ${targetAlias.TargetKeyId}`);
693
- return targetAlias;
694
- }
695
-
696
- console.log(`[KMS Alias Discovery] Alias ${aliasName} does not exist`);
697
- return null;
698
- } catch (error) {
699
- console.warn(
700
- `[KMS Alias Discovery] Error checking for alias ${aliasName}:`,
701
- error.message
702
- );
703
- return null;
704
- }
705
- }
706
-
707
- async findAuroraCluster(clusterIdentifier = null, serviceName = null, stage = null) {
708
- try {
709
- console.log('[AWSDiscovery.findAuroraCluster] Starting Aurora cluster discovery...');
710
-
711
- const command = new DescribeDBClustersCommand({});
712
- const response = await this.rdsClient.send(command);
713
-
714
- if (!response.DBClusters || response.DBClusters.length === 0) {
715
- console.log('[AWSDiscovery.findAuroraCluster] No Aurora clusters found');
716
- return null;
717
- }
718
-
719
- console.log(`[AWSDiscovery.findAuroraCluster] Found ${response.DBClusters.length} Aurora cluster(s)`);
720
-
721
- // Filter for Aurora PostgreSQL clusters
722
- const postgresClusters = response.DBClusters.filter(
723
- cluster => cluster.Engine === 'aurora-postgresql' && cluster.Status === 'available'
724
- );
725
-
726
- if (postgresClusters.length === 0) {
727
- console.log('[AWSDiscovery.findAuroraCluster] No available Aurora PostgreSQL clusters found');
728
- return null;
729
- }
730
-
731
- // Priority 1: User-specified cluster identifier
732
- if (clusterIdentifier) {
733
- const targetCluster = postgresClusters.find(
734
- cluster => cluster.DBClusterIdentifier === clusterIdentifier
735
- );
736
- if (targetCluster) {
737
- console.log(`[AWSDiscovery.findAuroraCluster] Found specified cluster: ${clusterIdentifier}`);
738
- return this._formatAuroraCluster(targetCluster);
739
- }
740
- console.warn(`[AWSDiscovery.findAuroraCluster] Specified cluster ${clusterIdentifier} not found`);
741
- return null;
742
- }
743
-
744
- // Priority 2: Frigg-managed cluster with matching service and stage tags
745
- if (serviceName && stage) {
746
- const friggCluster = postgresClusters.find(cluster => {
747
- const tags = cluster.TagList || [];
748
- const isFrigg = this._isFriggManaged(tags);
749
- const matchesService = tags.some(tag => tag.Key === 'Service' && tag.Value === serviceName);
750
- const matchesStage = tags.some(tag => tag.Key === 'Stage' && tag.Value === stage);
751
- return isFrigg && matchesService && matchesStage;
752
- });
753
-
754
- if (friggCluster) {
755
- console.log(`[AWSDiscovery.findAuroraCluster] Found Frigg-managed cluster: ${friggCluster.DBClusterIdentifier}`);
756
- return this._formatAuroraCluster(friggCluster);
757
- }
758
- }
759
-
760
- // Priority 3: Any Frigg-managed cluster
761
- const anyFriggCluster = postgresClusters.find(cluster =>
762
- this._isFriggManaged(cluster.TagList || [])
763
- );
764
-
765
- if (anyFriggCluster) {
766
- console.log(`[AWSDiscovery.findAuroraCluster] Found Frigg-managed cluster: ${anyFriggCluster.DBClusterIdentifier}`);
767
- return this._formatAuroraCluster(anyFriggCluster);
768
- }
769
-
770
- // Priority 4: First available cluster
771
- console.log(`[AWSDiscovery.findAuroraCluster] Using first available cluster: ${postgresClusters[0].DBClusterIdentifier}`);
772
- return this._formatAuroraCluster(postgresClusters[0]);
773
-
774
- } catch (error) {
775
- console.error('[AWSDiscovery.findAuroraCluster] Error finding Aurora cluster:', error.message);
776
- return null;
777
- }
778
- }
779
-
780
- async findDBSubnetGroup(vpcId) {
781
- try {
782
- console.log(`[AWSDiscovery.findDBSubnetGroup] Looking for DB subnet groups in VPC ${vpcId}...`);
783
-
784
- const command = new DescribeDBSubnetGroupsCommand({});
785
- const response = await this.rdsClient.send(command);
786
-
787
- if (!response.DBSubnetGroups || response.DBSubnetGroups.length === 0) {
788
- console.log('[AWSDiscovery.findDBSubnetGroup] No DB subnet groups found');
789
- return null;
790
- }
791
-
792
- // Filter by VPC ID
793
- const vpcSubnetGroups = response.DBSubnetGroups.filter(
794
- group => group.VpcId === vpcId
795
- );
796
-
797
- if (vpcSubnetGroups.length === 0) {
798
- console.log(`[AWSDiscovery.findDBSubnetGroup] No DB subnet groups found in VPC ${vpcId}`);
799
- return null;
800
- }
801
-
802
- // Priority 1: Frigg-managed subnet group
803
- const friggSubnetGroup = vpcSubnetGroups.find(group =>
804
- this._isFriggManaged(group.Tags || [])
805
- );
806
-
807
- if (friggSubnetGroup) {
808
- console.log(`[AWSDiscovery.findDBSubnetGroup] Found Frigg-managed subnet group: ${friggSubnetGroup.DBSubnetGroupName}`);
809
- return {
810
- name: friggSubnetGroup.DBSubnetGroupName,
811
- vpcId: friggSubnetGroup.VpcId,
812
- subnets: friggSubnetGroup.Subnets.map(s => s.SubnetIdentifier),
813
- description: friggSubnetGroup.DBSubnetGroupDescription
814
- };
815
- }
816
-
817
- // Priority 2: First available subnet group
818
- const subnetGroup = vpcSubnetGroups[0];
819
- console.log(`[AWSDiscovery.findDBSubnetGroup] Found subnet group: ${subnetGroup.DBSubnetGroupName}`);
820
- return {
821
- name: subnetGroup.DBSubnetGroupName,
822
- vpcId: subnetGroup.VpcId,
823
- subnets: subnetGroup.Subnets.map(s => s.SubnetIdentifier),
824
- description: subnetGroup.DBSubnetGroupDescription
825
- };
826
-
827
- } catch (error) {
828
- console.error('[AWSDiscovery.findDBSubnetGroup] Error finding DB subnet group:', error.message);
829
- return null;
830
- }
831
- }
832
-
833
- async findDatabaseSecret(serviceName, stage) {
834
- try {
835
- console.log(`[AWSDiscovery.findDatabaseSecret] Looking for database secret (service: ${serviceName}, stage: ${stage})...`);
836
-
837
- const command = new ListSecretsCommand({
838
- Filters: [
839
- {
840
- Key: 'tag-key',
841
- Values: ['ManagedBy']
842
- }
843
- ]
844
- });
845
- const response = await this.secretsManagerClient.send(command);
846
-
847
- if (!response.SecretList || response.SecretList.length === 0) {
848
- console.log('[AWSDiscovery.findDatabaseSecret] No secrets found');
849
- return null;
850
- }
851
-
852
- // Filter for Frigg-managed database secrets
853
- const friggSecrets = response.SecretList.filter(secret => {
854
- const tags = secret.Tags || [];
855
- const isFrigg = this._isFriggManaged(tags);
856
- const isDatabase = secret.Name?.includes('aurora') || secret.Name?.includes('database');
857
- return isFrigg && isDatabase;
858
- });
859
-
860
- if (friggSecrets.length === 0) {
861
- console.log('[AWSDiscovery.findDatabaseSecret] No Frigg-managed database secrets found');
862
- return null;
863
- }
864
-
865
- // Priority 1: Secret with matching service and stage tags
866
- if (serviceName && stage) {
867
- const matchingSecret = friggSecrets.find(secret => {
868
- const tags = secret.Tags || [];
869
- const matchesService = tags.some(tag => tag.Key === 'Service' && tag.Value === serviceName);
870
- const matchesStage = tags.some(tag => tag.Key === 'Stage' && tag.Value === stage);
871
- return matchesService && matchesStage;
872
- });
873
-
874
- if (matchingSecret) {
875
- console.log(`[AWSDiscovery.findDatabaseSecret] Found matching secret: ${matchingSecret.Name}`);
876
- return {
877
- arn: matchingSecret.ARN,
878
- name: matchingSecret.Name
879
- };
880
- }
881
- }
882
-
883
- // Priority 2: First Frigg-managed database secret
884
- const secret = friggSecrets[0];
885
- console.log(`[AWSDiscovery.findDatabaseSecret] Found Frigg-managed secret: ${secret.Name}`);
886
- return {
887
- arn: secret.ARN,
888
- name: secret.Name
889
- };
890
-
891
- } catch (error) {
892
- console.error('[AWSDiscovery.findDatabaseSecret] Error finding database secret:', error.message);
893
- return null;
894
- }
895
- }
896
-
897
- async discoverAuroraResources(options = {}) {
898
- try {
899
- console.log('\n🔍 Discovering Aurora PostgreSQL resources...');
900
- console.log('═'.repeat(60));
901
-
902
- const {
903
- vpcId,
904
- serviceName,
905
- stage,
906
- management = 'discover',
907
- clusterIdentifier = null
908
- } = options;
909
-
910
- const result = {
911
- clusterIdentifier: null,
912
- endpoint: null,
913
- port: null,
914
- engine: null,
915
- engineVersion: null,
916
- status: null,
917
- dbSubnetGroupName: null,
918
- secretArn: null,
919
- secretName: null,
920
- needsCreation: false
921
- };
922
-
923
- // For 'use-existing' mode, cluster identifier is required
924
- if (management === 'use-existing' && !clusterIdentifier) {
925
- throw new Error('clusterIdentifier is required when management mode is "use-existing"');
926
- }
927
-
928
- // For 'create-new' mode, skip discovery
929
- if (management === 'create-new') {
930
- console.log('💡 Management mode is "create-new" - will provision new Aurora cluster');
931
- result.needsCreation = true;
932
- return result;
933
- }
934
-
935
- // Discover Aurora cluster
936
- const cluster = await this.findAuroraCluster(clusterIdentifier, serviceName, stage);
937
-
938
- if (!cluster) {
939
- if (management === 'discover') {
940
- console.log('⚠️ No Aurora cluster found - will provision new cluster');
941
- result.needsCreation = true;
942
- return result;
943
- }
944
- throw new Error(`No Aurora cluster found with identifier: ${clusterIdentifier}`);
945
- }
946
-
947
- result.clusterIdentifier = cluster.identifier;
948
- result.endpoint = cluster.endpoint;
949
- result.port = cluster.port;
950
- result.engine = cluster.engine;
951
- result.engineVersion = cluster.engineVersion;
952
- result.status = cluster.status;
953
- result.masterUsername = cluster.masterUsername;
954
- result.isFriggManaged = cluster.isFriggManaged;
955
-
956
- console.log(`\n✅ Found Aurora Cluster: ${cluster.identifier}`);
957
- console.log(` Endpoint: ${cluster.endpoint}:${cluster.port}`);
958
- console.log(` Engine: ${cluster.engine} ${cluster.engineVersion}`);
959
- console.log(` Status: ${cluster.status}`);
960
-
961
- // Discover DB subnet group
962
- const subnetGroup = await this.findDBSubnetGroup(vpcId);
963
- if (subnetGroup) {
964
- result.dbSubnetGroupName = subnetGroup.name;
965
- console.log(`\n✅ Found DB Subnet Group: ${subnetGroup.name}`);
966
- console.log(` Subnets: ${subnetGroup.subnets.join(', ')}`);
967
- }
968
-
969
- // Discover database secret
970
- const secret = await this.findDatabaseSecret(serviceName, stage);
971
- if (secret) {
972
- result.secretArn = secret.arn;
973
- result.secretName = secret.name;
974
- console.log(`\n✅ Found Database Secret: ${secret.name}`);
975
- }
976
-
977
- console.log(`\n${'═'.repeat(60)}`);
978
- console.log('📋 Aurora Discovery Summary:');
979
- console.log(` Cluster: ${result.clusterIdentifier || 'Not found'}`);
980
- console.log(` Subnet Group: ${result.dbSubnetGroupName || 'Not found'}`);
981
- console.log(` Secret: ${result.secretName || 'Not found'}`);
982
- console.log(`${'═'.repeat(60)}\n`);
983
-
984
- return result;
985
-
986
- } catch (error) {
987
- console.error('❌ Aurora resource discovery failed:', error.message);
988
- throw error;
989
- }
990
- }
991
-
992
- _formatAuroraCluster(cluster) {
993
- return {
994
- identifier: cluster.DBClusterIdentifier,
995
- endpoint: cluster.Endpoint,
996
- readerEndpoint: cluster.ReaderEndpoint,
997
- port: cluster.Port,
998
- engine: cluster.Engine,
999
- engineVersion: cluster.EngineVersion,
1000
- status: cluster.Status,
1001
- masterUsername: cluster.MasterUsername,
1002
- databaseName: cluster.DatabaseName,
1003
- vpcSecurityGroups: (cluster.VpcSecurityGroups || []).map(sg => sg.VpcSecurityGroupId),
1004
- dbSubnetGroup: cluster.DBSubnetGroup,
1005
- arn: cluster.DBClusterArn,
1006
- isFriggManaged: this._isFriggManaged(cluster.TagList || [])
1007
- };
1008
- }
1009
-
1010
- async detectMisconfiguredResources(vpcId) {
1011
- try {
1012
- const misconfigurations = {
1013
- natGatewaysInPrivateSubnets: [],
1014
- orphanedElasticIps: [],
1015
- misconfiguredRouteTables: [],
1016
- privateSubnetsWithoutNatRoute: [],
1017
- };
1018
-
1019
- const natCommand = new DescribeNatGatewaysCommand({
1020
- Filter: [
1021
- { Name: 'vpc-id', Values: [vpcId] },
1022
- { Name: 'state', Values: ['available'] },
1023
- ],
1024
- });
1025
- const natResponse = await this.ec2Client.send(natCommand);
1026
-
1027
- for (const nat of natResponse.NatGateways || []) {
1028
- const isPrivate = await this.isSubnetPrivate(nat.SubnetId, vpcId);
1029
- if (isPrivate) {
1030
- misconfigurations.natGatewaysInPrivateSubnets.push({
1031
- natGatewayId: nat.NatGatewayId,
1032
- subnetId: nat.SubnetId,
1033
- tags: nat.Tags,
1034
- });
1035
- }
1036
- }
1037
-
1038
- const eipCommand = new DescribeAddressesCommand({});
1039
- const eipResponse = await this.ec2Client.send(eipCommand);
1040
-
1041
- for (const eip of eipResponse.Addresses || []) {
1042
- if (
1043
- !eip.InstanceId &&
1044
- !eip.NetworkInterfaceId &&
1045
- !eip.AssociationId &&
1046
- this._isFriggManaged(eip.Tags)
1047
- ) {
1048
- misconfigurations.orphanedElasticIps.push({
1049
- allocationId: eip.AllocationId,
1050
- publicIp: eip.PublicIp,
1051
- tags: eip.Tags,
1052
- });
1053
- }
1054
- }
1055
-
1056
- const subnets = await this.findPrivateSubnets(vpcId);
1057
- const routeTables = await this.findRouteTables(vpcId);
1058
-
1059
- for (const subnet of subnets) {
1060
- const hasNatRoute = routeTables.some((rt) => {
1061
- const isAssociated = (rt.Associations || []).some(
1062
- (assoc) => assoc.SubnetId === subnet.SubnetId
1063
- );
1064
- if (!isAssociated) {
1065
- return false;
1066
- }
1067
- return (rt.Routes || []).some(
1068
- (route) =>
1069
- route.NatGatewayId &&
1070
- route.DestinationCidrBlock === '0.0.0.0/0'
1071
- );
1072
- });
1073
-
1074
- if (!hasNatRoute) {
1075
- misconfigurations.privateSubnetsWithoutNatRoute.push({
1076
- subnetId: subnet.SubnetId,
1077
- availabilityZone: subnet.AvailabilityZone,
1078
- });
1079
- }
1080
- }
1081
-
1082
- return misconfigurations;
1083
- } catch (error) {
1084
- console.error('Error detecting misconfigurations:', error);
1085
- return {
1086
- natGatewaysInPrivateSubnets: [],
1087
- orphanedElasticIps: [],
1088
- misconfiguredRouteTables: [],
1089
- privateSubnetsWithoutNatRoute: [],
1090
- };
1091
- }
1092
- }
1093
-
1094
- getHealingRecommendations(misconfigurations) {
1095
- const recommendations = [];
1096
-
1097
- if (misconfigurations.natGatewaysInPrivateSubnets.length > 0) {
1098
- recommendations.push({
1099
- severity: 'critical',
1100
- issue: 'NAT Gateway in private subnet',
1101
- recommendation:
1102
- 'Recreate NAT Gateway in public subnet or fix route tables',
1103
- affectedResources:
1104
- misconfigurations.natGatewaysInPrivateSubnets.map(
1105
- (n) => n.natGatewayId
1106
- ),
1107
- });
1108
- }
1109
-
1110
- if (misconfigurations.orphanedElasticIps.length > 0) {
1111
- recommendations.push({
1112
- severity: 'warning',
1113
- issue: 'Orphaned Elastic IPs',
1114
- recommendation: 'Release unused Elastic IPs to avoid charges',
1115
- affectedResources: misconfigurations.orphanedElasticIps.map(
1116
- (e) => e.allocationId
1117
- ),
1118
- });
1119
- }
1120
-
1121
- if (misconfigurations.privateSubnetsWithoutNatRoute.length > 0) {
1122
- recommendations.push({
1123
- severity: 'critical',
1124
- issue: 'Private subnets without NAT route',
1125
- recommendation:
1126
- 'Add NAT Gateway route to private subnet route tables',
1127
- affectedResources:
1128
- misconfigurations.privateSubnetsWithoutNatRoute.map(
1129
- (s) => s.subnetId
1130
- ),
1131
- });
1132
- }
1133
-
1134
- recommendations.sort((a, b) => {
1135
- const severityOrder = { critical: 0, warning: 1, info: 2 };
1136
- return severityOrder[a.severity] - severityOrder[b.severity];
1137
- });
1138
-
1139
- return recommendations;
1140
- }
1141
-
1142
- async discoverResources(options = {}) {
1143
- try {
1144
- console.log(
1145
- '\n🚀 Discovering AWS resources for Frigg deployment...'
1146
- );
1147
- console.log('═'.repeat(60));
1148
-
1149
- // Validate credentials before attempting any AWS operations
1150
- await this.validateCredentials();
1151
- console.log(''); // Add spacing after validation
1152
-
1153
- const vpc = await this.findDefaultVpc();
1154
- console.log(`\n✅ Found VPC: ${vpc.VpcId}`);
1155
-
1156
- const autoConvert = options.vpc?.selfHeal || false;
1157
-
1158
- const privateSubnets = await this.findPrivateSubnets(
1159
- vpc.VpcId,
1160
- autoConvert
1161
- );
1162
- console.log(
1163
- `\n✅ Selected subnets for Lambda: ${privateSubnets
1164
- .map((s) => s.SubnetId)
1165
- .join(', ')}`
1166
- );
1167
-
1168
- const publicSubnets = await this.findPublicSubnets(vpc.VpcId);
1169
- if (publicSubnets.primary) {
1170
- console.log(
1171
- `\n✅ Found public subnet(s) for NAT Gateway and Aurora`
1172
- );
1173
- } else {
1174
- console.log(
1175
- `\n⚠️ No public subnet found - NAT Gateway creation may fail`
1176
- );
1177
- }
1178
-
1179
- const securityGroup = await this.findDefaultSecurityGroup(
1180
- vpc.VpcId
1181
- );
1182
- console.log(`\n✅ Found security group: ${securityGroup.GroupId}`);
1183
-
1184
- const routeTable = await this.findPrivateRouteTable(vpc.VpcId);
1185
- console.log(`✅ Found route table: ${routeTable.RouteTableId}`);
1186
-
1187
- const kmsKeyArn = await this.findDefaultKmsKey();
1188
- if (kmsKeyArn) {
1189
- console.log(`✅ Found KMS key: ${kmsKeyArn}`);
1190
- } else {
1191
- console.log('ℹ️ No KMS key found');
1192
- }
1193
-
1194
- // Check if KMS alias already exists
1195
- let kmsAliasExists = false;
1196
- if (options.serviceName && options.stage) {
1197
- const aliasName = `alias/${options.serviceName}-${options.stage}-frigg-kms`;
1198
- const existingAlias = await this.findKmsAlias(aliasName);
1199
- kmsAliasExists = existingAlias !== null;
1200
- }
1201
-
1202
- // Discover Aurora PostgreSQL resources if enabled
1203
- let auroraResources = {};
1204
- if (options.database?.postgres?.enable) {
1205
- auroraResources = await this.discoverAuroraResources({
1206
- vpcId: vpc.VpcId,
1207
- serviceName: options.serviceName,
1208
- stage: options.stage,
1209
- management: options.database.postgres.management,
1210
- clusterIdentifier: options.database.postgres.clusterIdentifier,
1211
- });
1212
- }
1213
-
1214
- const existingNatGateway = await this.findExistingNatGateway(
1215
- vpc.VpcId
1216
- );
1217
- let natGatewayId = null;
1218
- let elasticIpAllocationId = null;
1219
- let natGatewayInPrivateSubnet = false;
1220
-
1221
- if (existingNatGateway) {
1222
- natGatewayId = existingNatGateway.NatGatewayId;
1223
- natGatewayInPrivateSubnet =
1224
- existingNatGateway._isInPrivateSubnet || false;
1225
-
1226
- if (
1227
- existingNatGateway.NatGatewayAddresses &&
1228
- existingNatGateway.NatGatewayAddresses.length > 0
1229
- ) {
1230
- elasticIpAllocationId =
1231
- existingNatGateway.NatGatewayAddresses[0].AllocationId;
1232
- }
1233
- } else {
1234
- const availableEIP = await this.findAvailableElasticIP();
1235
- if (availableEIP) {
1236
- elasticIpAllocationId = availableEIP.AllocationId;
1237
- }
1238
- }
1239
-
1240
- const subnet1IsActuallyPrivate = privateSubnets[0]
1241
- ? await this.isSubnetPrivate(
1242
- privateSubnets[0].SubnetId,
1243
- privateSubnets[0].VpcId || vpc.VpcId
1244
- )
1245
- : false;
1246
- const subnet2IsActuallyPrivate = privateSubnets[1]
1247
- ? await this.isSubnetPrivate(
1248
- privateSubnets[1].SubnetId,
1249
- privateSubnets[1].VpcId || vpc.VpcId
1250
- )
1251
- : subnet1IsActuallyPrivate;
1252
-
1253
- const subnetStatus = {
1254
- requiresConversion:
1255
- !subnet1IsActuallyPrivate || !subnet2IsActuallyPrivate,
1256
- subnet1NeedsConversion: !subnet1IsActuallyPrivate,
1257
- subnet2NeedsConversion: !subnet2IsActuallyPrivate,
1258
- };
1259
-
1260
- if (subnetStatus.requiresConversion) {
1261
- console.log(`\n⚠️ SUBNET CONFIGURATION WARNING:`);
1262
- if (subnetStatus.subnet1NeedsConversion && privateSubnets[0]) {
1263
- console.log(
1264
- ` - Subnet ${privateSubnets[0].SubnetId} is currently PUBLIC but will be used for Lambda`
1265
- );
1266
- }
1267
- if (subnetStatus.subnet2NeedsConversion && privateSubnets[1]) {
1268
- console.log(
1269
- ` - Subnet ${privateSubnets[1].SubnetId} is currently PUBLIC but will be used for Lambda`
1270
- );
1271
- }
1272
- console.log(
1273
- ` 💡 Enable selfHeal: true to automatically fix this`
1274
- );
1275
- }
1276
-
1277
- console.log(`\n${'═'.repeat(60)}`);
1278
- console.log('📋 Discovery Summary:');
1279
- console.log(` VPC: ${vpc.VpcId}`);
1280
- console.log(
1281
- ` Lambda Subnets: ${privateSubnets
1282
- .map((s) => s.SubnetId)
1283
- .join(', ')}`
1284
- );
1285
- console.log(
1286
- ` NAT Subnet: ${publicSubnets.primary?.SubnetId || 'None (needs creation)'
1287
- }`
1288
- );
1289
- if (publicSubnets.secondary && publicSubnets.secondary.SubnetId !== publicSubnets.primary?.SubnetId) {
1290
- console.log(
1291
- ` Aurora Public Subnet 2: ${publicSubnets.secondary.SubnetId}`
1292
- );
1293
- }
1294
- console.log(
1295
- ` NAT Gateway: ${natGatewayId || 'None (will be created)'}`
1296
- );
1297
- console.log(
1298
- ` Elastic IP: ${elasticIpAllocationId || 'None (will be allocated)'
1299
- }`
1300
- );
1301
- if (subnetStatus.requiresConversion) {
1302
- console.log(` ⚠️ Subnet Conversion Required: Yes`);
1303
- }
1304
- console.log(`${'═'.repeat(60)}\n`);
1305
-
1306
- return {
1307
- defaultVpcId: vpc.VpcId,
1308
- vpcCidr: vpc.CidrBlock,
1309
- defaultSecurityGroupId: securityGroup.GroupId,
1310
- privateSubnetId1: privateSubnets[0]?.SubnetId,
1311
- privateSubnetId2:
1312
- privateSubnets[1]?.SubnetId || privateSubnets[0]?.SubnetId,
1313
- publicSubnetId: publicSubnets.primary?.SubnetId || null, // Keep for NAT Gateway backward compat
1314
- publicSubnetId1: publicSubnets.primary?.SubnetId || null,
1315
- publicSubnetId2: publicSubnets.secondary?.SubnetId || null,
1316
- privateRouteTableId: routeTable.RouteTableId,
1317
- defaultKmsKeyId: kmsKeyArn,
1318
- kmsAliasExists: kmsAliasExists,
1319
- existingNatGatewayId: natGatewayId,
1320
- existingElasticIpAllocationId: elasticIpAllocationId,
1321
- natGatewayInPrivateSubnet: natGatewayInPrivateSubnet,
1322
- subnetConversionRequired: subnetStatus.requiresConversion,
1323
- privateSubnetsWithWrongRoutes: (() => {
1324
- const wrongRoutes = [];
1325
- if (
1326
- subnetStatus.subnet1NeedsConversion &&
1327
- privateSubnets[0]
1328
- ) {
1329
- wrongRoutes.push(privateSubnets[0].SubnetId);
1330
- }
1331
- if (
1332
- subnetStatus.subnet2NeedsConversion &&
1333
- privateSubnets[1]
1334
- ) {
1335
- wrongRoutes.push(privateSubnets[1].SubnetId);
1336
- }
1337
- return wrongRoutes;
1338
- })(),
1339
- aurora: auroraResources,
1340
- };
1341
- } catch (error) {
1342
- console.error('Error discovering AWS resources:', error);
1343
- throw error;
1344
- }
1345
- }
1346
-
1347
- async findInternetGateway(vpcId) {
1348
- try {
1349
- const command = new DescribeInternetGatewaysCommand({
1350
- Filters: [
1351
- {
1352
- Name: 'attachment.vpc-id',
1353
- Values: [vpcId],
1354
- },
1355
- {
1356
- Name: 'attachment.state',
1357
- Values: ['available'],
1358
- },
1359
- ],
1360
- });
1361
-
1362
- const response = await this.ec2Client.send(command);
1363
-
1364
- if (
1365
- response.InternetGateways &&
1366
- response.InternetGateways.length > 0
1367
- ) {
1368
- console.log(
1369
- `Found existing Internet Gateway: ${response.InternetGateways[0].InternetGatewayId}`
1370
- );
1371
- return response.InternetGateways[0];
1372
- }
1373
-
1374
- return null;
1375
- } catch (error) {
1376
- console.warn('Error finding Internet Gateway:', error.message);
1377
- return null;
1378
- }
1379
- }
1380
-
1381
- async findFriggManagedResources(serviceName, stage) {
1382
- const results = {
1383
- natGateways: [],
1384
- elasticIps: [],
1385
- routeTables: [],
1386
- subnets: [],
1387
- securityGroups: [],
1388
- };
1389
-
1390
- try {
1391
- const filters = [
1392
- {
1393
- Name: 'tag:ManagedBy',
1394
- Values: ['Frigg'],
1395
- },
1396
- ];
1397
-
1398
- if (serviceName) {
1399
- filters.push({
1400
- Name: 'tag:Service',
1401
- Values: [serviceName],
1402
- });
1403
- }
1404
-
1405
- if (stage) {
1406
- filters.push({
1407
- Name: 'tag:Stage',
1408
- Values: [stage],
1409
- });
1410
- }
1411
-
1412
- const fetchWithFallback = async (Command, input, field, label) => {
1413
- try {
1414
- const response = await this.ec2Client.send(
1415
- new Command(input)
1416
- );
1417
- return response[field] || [];
1418
- } catch (err) {
1419
- console.warn(
1420
- `Error finding Frigg ${label}:`,
1421
- err.message
1422
- );
1423
- return [];
1424
- }
1425
- };
1426
-
1427
- results.natGateways = await fetchWithFallback(
1428
- DescribeNatGatewaysCommand,
1429
- {
1430
- Filter: [
1431
- ...filters,
1432
- {
1433
- Name: 'state',
1434
- Values: ['available'],
1435
- },
1436
- ],
1437
- },
1438
- 'NatGateways',
1439
- 'NAT Gateways'
1440
- );
1441
-
1442
- results.elasticIps = await fetchWithFallback(
1443
- DescribeAddressesCommand,
1444
- { Filters: filters },
1445
- 'Addresses',
1446
- 'Elastic IPs'
1447
- );
1448
-
1449
- results.routeTables = await fetchWithFallback(
1450
- DescribeRouteTablesCommand,
1451
- { Filters: filters },
1452
- 'RouteTables',
1453
- 'Route Tables'
1454
- );
1455
-
1456
- results.subnets = await fetchWithFallback(
1457
- DescribeSubnetsCommand,
1458
- { Filters: filters },
1459
- 'Subnets',
1460
- 'Subnets'
1461
- );
1462
-
1463
- results.securityGroups = await fetchWithFallback(
1464
- DescribeSecurityGroupsCommand,
1465
- { Filters: filters },
1466
- 'SecurityGroups',
1467
- 'Security Groups'
1468
- );
1469
-
1470
- console.log('Found Frigg-managed resources:', {
1471
- natGateways: results.natGateways.length,
1472
- elasticIps: results.elasticIps.length,
1473
- routeTables: results.routeTables.length,
1474
- subnets: results.subnets.length,
1475
- securityGroups: results.securityGroups.length,
1476
- });
1477
-
1478
- return results;
1479
- } catch (error) {
1480
- console.error('Error finding Frigg-managed resources:', error);
1481
- return results;
1482
- }
1483
- }
1484
-
1485
- async findRouteTables(vpcId) {
1486
- const command = new DescribeRouteTablesCommand({
1487
- Filters: [
1488
- {
1489
- Name: 'vpc-id',
1490
- Values: [vpcId],
1491
- },
1492
- ],
1493
- });
1494
- const response = await this.ec2Client.send(command);
1495
- return response.RouteTables || [];
1496
- }
1497
-
1498
- async _fetchSubnets(vpcId) {
1499
- const command = new DescribeSubnetsCommand({
1500
- Filters: [
1501
- {
1502
- Name: 'vpc-id',
1503
- Values: [vpcId],
1504
- },
1505
- ],
1506
- });
1507
- const response = await this.ec2Client.send(command);
1508
- return response.Subnets || [];
1509
- }
1510
-
1511
- async _getSubnetVpcId(subnetId) {
1512
- const command = new DescribeSubnetsCommand({
1513
- SubnetIds: [subnetId],
1514
- });
1515
- const response = await this.ec2Client.send(command);
1516
-
1517
- if (!response.Subnets || response.Subnets.length === 0) {
1518
- throw new Error(`Subnet ${subnetId} not found`);
1519
- }
1520
-
1521
- return response.Subnets[0].VpcId;
1522
- }
1523
-
1524
- async _classifySubnets(subnets, { logDetails = false } = {}) {
1525
- const privateSubnets = [];
1526
- const publicSubnets = [];
1527
-
1528
- for (const subnet of subnets) {
1529
- const isPrivate = await this.isSubnetPrivate(
1530
- subnet.SubnetId,
1531
- subnet.VpcId
1532
- );
1533
- if (isPrivate) {
1534
- privateSubnets.push(subnet);
1535
- if (logDetails) {
1536
- console.log(
1537
- ` 🔒 Private subnet: ${subnet.SubnetId} (AZ: ${subnet.AvailabilityZone})`
1538
- );
1539
- }
1540
- } else {
1541
- publicSubnets.push(subnet);
1542
- if (logDetails) {
1543
- console.log(
1544
- ` 🌐 Public subnet: ${subnet.SubnetId} (AZ: ${subnet.AvailabilityZone})`
1545
- );
1546
- }
1547
- }
1548
- }
1549
-
1550
- return { privateSubnets, publicSubnets };
1551
- }
1552
-
1553
- _logSubnetSummary(privateCount, publicCount) {
1554
- console.log(`\n📊 Subnet Analysis Results:`);
1555
- console.log(` - Private subnets: ${privateCount}`);
1556
- console.log(` - Public subnets: ${publicCount}`);
1557
- }
1558
-
1559
- _selectSubnetsForLambda({ privateSubnets, publicSubnets, autoConvert, vpcId }) {
1560
- if (privateSubnets.length >= 2) {
1561
- console.log(
1562
- `✅ Found ${privateSubnets.length} private subnets for Lambda deployment`
1563
- );
1564
- return privateSubnets.slice(0, 2);
1565
- }
1566
-
1567
- if (privateSubnets.length === 1) {
1568
- console.warn(
1569
- `⚠️ Only 1 private subnet found. Need at least 2 for high availability.`
1570
- );
1571
- if (publicSubnets.length > 0 && autoConvert) {
1572
- console.log(
1573
- `🔄 Will convert 1 public subnet to private for high availability...`
1574
- );
1575
- }
1576
- return [...privateSubnets, ...publicSubnets].slice(0, 2);
1577
- }
1578
-
1579
- if (privateSubnets.length === 0 && publicSubnets.length > 0) {
1580
- console.error(
1581
- `❌ CRITICAL: No private subnets found, but ${publicSubnets.length} public subnets exist`
1582
- );
1583
- console.error(
1584
- `❌ Lambda functions should NOT be deployed in public subnets!`
1585
- );
1586
-
1587
- if (autoConvert && publicSubnets.length >= 3) {
1588
- console.log(
1589
- `\n🔧 AUTO-CONVERSION: Will configure subnets for proper isolation...`
1590
- );
1591
- console.log(
1592
- ` - Keeping ${publicSubnets[0].SubnetId} as public (for NAT Gateway)`
1593
- );
1594
- console.log(
1595
- ` - Converting ${publicSubnets[1].SubnetId} to private (for Lambda)`
1596
- );
1597
- if (publicSubnets[2]) {
1598
- console.log(
1599
- ` - Converting ${publicSubnets[2].SubnetId} to private (for Lambda)`
1600
- );
1601
- }
1602
- return publicSubnets.slice(1, 3);
1603
- }
1604
-
1605
- if (autoConvert && publicSubnets.length >= 2) {
1606
- console.log(
1607
- `\n🔧 AUTO-CONVERSION: Only ${publicSubnets.length} subnets available`
1608
- );
1609
- console.log(
1610
- ` - Will need to create new subnets or reconfigure existing ones`
1611
- );
1612
- return publicSubnets.slice(0, 2);
1613
- }
1614
-
1615
- console.error(`\n⚠️ CONFIGURATION ERROR:`);
1616
- console.error(
1617
- ` Found ${publicSubnets.length} public subnets but no private subnets.`
1618
- );
1619
- console.error(
1620
- ` Lambda functions require private subnets for security.`
1621
- );
1622
- console.error(`\n Options:`);
1623
- console.error(
1624
- ` 1. Enable selfHeal: true in vpc configuration`
1625
- );
1626
- console.error(` 2. Create private subnets manually`);
1627
- console.error(
1628
- ` 3. Set subnets.management: 'create' to create new private subnets`
1629
- );
1630
-
1631
- throw new Error(
1632
- `No private subnets found in VPC ${vpcId}. ` +
1633
- `Found ${publicSubnets.length} public subnets. ` +
1634
- `Lambda requires private subnets. Enable selfHeal or create private subnets.`
1635
- );
1636
- }
1637
-
1638
- return null;
1639
- }
1640
-
1641
- _findRouteTableForSubnet(routeTables, subnetId) {
1642
- for (const rt of routeTables) {
1643
- for (const assoc of rt.Associations || []) {
1644
- if (assoc.SubnetId === subnetId) {
1645
- return rt;
1646
- }
1647
- }
1648
- }
1649
-
1650
- for (const rt of routeTables) {
1651
- for (const assoc of rt.Associations || []) {
1652
- if (assoc.Main === true) {
1653
- return rt;
1654
- }
1655
- }
1656
- }
1657
-
1658
- return null;
1659
- }
1660
-
1661
- _findIgwRoute(routeTable) {
1662
- for (const route of routeTable.Routes || []) {
1663
- if (route.GatewayId && route.GatewayId.startsWith('igw-')) {
1664
- return route.GatewayId;
1665
- }
1666
- }
1667
- return null;
1668
- }
1669
-
1670
- _isFriggManaged(tags) {
1671
- if (!tags) {
1672
- return false;
1673
- }
1674
-
1675
- return tags.some(
1676
- (tag) =>
1677
- (tag.Key === 'ManagedBy' && tag.Value === 'Frigg') ||
1678
- (tag.Key === 'Name' &&
1679
- typeof tag.Value === 'string' &&
1680
- tag.Value.includes('frigg'))
1681
- );
1682
- }
1683
-
1684
- async _findSecurityGroupByName(vpcId, groupName) {
1685
- const command = new DescribeSecurityGroupsCommand({
1686
- Filters: [
1687
- {
1688
- Name: 'vpc-id',
1689
- Values: [vpcId],
1690
- },
1691
- {
1692
- Name: 'group-name',
1693
- Values: [groupName],
1694
- },
1695
- ],
1696
- });
1697
-
1698
- const response = await this.ec2Client.send(command);
1699
- const groups = response.SecurityGroups || [];
1700
- return groups[0] || null;
1701
- }
1702
- }
1703
-
1704
- module.exports = { AWSDiscovery };