@friggframework/devtools 2.0.0--canary.474.a0b734c.0 → 2.0.0--canary.474.898a56c.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 (24) hide show
  1. package/infrastructure/domains/health/application/use-cases/__tests__/mismatch-analyzer-method-name.test.js +167 -0
  2. package/infrastructure/domains/health/application/use-cases/__tests__/repair-via-import-use-case.test.js +762 -0
  3. package/infrastructure/domains/health/application/use-cases/repair-via-import-use-case.js +154 -1
  4. package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.js +20 -5
  5. package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.test.js +3 -3
  6. package/infrastructure/domains/health/docs/ACME-DEV-DRIFT-ANALYSIS.md +267 -0
  7. package/infrastructure/domains/health/docs/BUILD-VS-DEPLOYED-TEMPLATE-ANALYSIS.md +324 -0
  8. package/infrastructure/domains/health/docs/ORPHAN-DETECTION-ANALYSIS.md +386 -0
  9. package/infrastructure/domains/health/docs/SPEC-CLEANUP-COMMAND.md +1419 -0
  10. package/infrastructure/domains/health/docs/TDD-IMPLEMENTATION-SUMMARY.md +391 -0
  11. package/infrastructure/domains/health/docs/TEMPLATE-COMPARISON-IMPLEMENTATION.md +551 -0
  12. package/infrastructure/domains/health/domain/services/__tests__/health-score-percentage-based.test.js +380 -0
  13. package/infrastructure/domains/health/domain/services/__tests__/logical-id-mapper.test.js +645 -0
  14. package/infrastructure/domains/health/domain/services/__tests__/template-parser.test.js +496 -0
  15. package/infrastructure/domains/health/domain/services/health-score-calculator.js +174 -91
  16. package/infrastructure/domains/health/domain/services/health-score-calculator.test.js +332 -228
  17. package/infrastructure/domains/health/domain/services/logical-id-mapper.js +330 -0
  18. package/infrastructure/domains/health/domain/services/template-parser.js +245 -0
  19. package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-cfn-tagged.test.js +312 -0
  20. package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-multi-stack.test.js +367 -0
  21. package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-relationship-analysis.test.js +432 -0
  22. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector.js +108 -14
  23. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector.test.js +69 -12
  24. package/package.json +6 -6
@@ -12,8 +12,12 @@
12
12
  * - Generate CloudFormation template snippets
13
13
  * - Execute import operations (single or batch)
14
14
  * - Track import operation status
15
+ * - Map orphaned resources to correct logical IDs using template comparison
15
16
  */
16
17
 
18
+ const { TemplateParser } = require('../../domain/services/template-parser');
19
+ const { LogicalIdMapper } = require('../../domain/services/logical-id-mapper');
20
+
17
21
  class RepairViaImportUseCase {
18
22
  /**
19
23
  * Create use case with required dependencies
@@ -21,8 +25,11 @@ class RepairViaImportUseCase {
21
25
  * @param {Object} params
22
26
  * @param {IResourceImporter} params.resourceImporter - Resource import operations
23
27
  * @param {IResourceDetector} params.resourceDetector - Resource discovery and details
28
+ * @param {IStackRepository} params.stackRepository - CloudFormation stack operations
29
+ * @param {TemplateParser} params.templateParser - CloudFormation template parsing
30
+ * @param {LogicalIdMapper} params.logicalIdMapper - Logical ID mapping service
24
31
  */
25
- constructor({ resourceImporter, resourceDetector }) {
32
+ constructor({ resourceImporter, resourceDetector, stackRepository, templateParser, logicalIdMapper }) {
26
33
  if (!resourceImporter) {
27
34
  throw new Error('resourceImporter is required');
28
35
  }
@@ -32,6 +39,9 @@ class RepairViaImportUseCase {
32
39
 
33
40
  this.resourceImporter = resourceImporter;
34
41
  this.resourceDetector = resourceDetector;
42
+ this.stackRepository = stackRepository;
43
+ this.templateParser = templateParser || new TemplateParser();
44
+ this.logicalIdMapper = logicalIdMapper || new LogicalIdMapper({ region: 'us-east-1' });
35
45
  }
36
46
 
37
47
  /**
@@ -224,6 +234,149 @@ class RepairViaImportUseCase {
224
234
  properties: resourceDetails.properties,
225
235
  };
226
236
  }
237
+
238
+ /**
239
+ * Import orphaned resources with automatic logical ID mapping
240
+ * Uses template comparison to find correct logical IDs
241
+ *
242
+ * @param {Object} params
243
+ * @param {StackIdentifier} params.stackIdentifier - Target stack
244
+ * @param {Array} params.orphanedResources - Orphaned resources to import
245
+ * @param {string} params.buildTemplatePath - Path to .serverless/cloudformation-template-update-stack.json
246
+ * @returns {Promise<Object>} Import result with mappings
247
+ */
248
+ async importWithLogicalIdMapping({ stackIdentifier, orphanedResources, buildTemplatePath }) {
249
+ // 1. Validate build template exists
250
+ if (!buildTemplatePath) {
251
+ throw new Error('buildTemplatePath is required');
252
+ }
253
+
254
+ const fs = require('fs');
255
+ if (!fs.existsSync(buildTemplatePath)) {
256
+ throw new Error(
257
+ `Build template not found at: ${buildTemplatePath}\n\n` +
258
+ `Please run one of:\n` +
259
+ ` • serverless package\n` +
260
+ ` • frigg build\n` +
261
+ ` • frigg deploy --stage dev\n\n` +
262
+ `Then try again:\n` +
263
+ ` frigg repair --import ${stackIdentifier.stackName}`
264
+ );
265
+ }
266
+
267
+ // 2. Parse build template
268
+ const buildTemplate = this.templateParser.parseTemplate(buildTemplatePath);
269
+
270
+ // 3. Get deployed template from CloudFormation
271
+ if (!this.stackRepository) {
272
+ throw new Error('stackRepository is required for template comparison');
273
+ }
274
+
275
+ const deployedTemplate = await this.stackRepository.getTemplate(stackIdentifier);
276
+
277
+ // 4. Map orphaned resources to logical IDs
278
+ const mappings = await this.logicalIdMapper.mapOrphanedResourcesToLogicalIds({
279
+ orphanedResources,
280
+ buildTemplate,
281
+ deployedTemplate,
282
+ });
283
+
284
+ // 5. Check for multiple resources of same type
285
+ const multiResourceWarnings = this._checkForMultipleResources(mappings);
286
+
287
+ // 6. Filter out unmapped resources and prepare for import
288
+ const mappedResources = mappings.filter((m) => m.logicalId !== null);
289
+ const unmappedResources = mappings.filter((m) => m.logicalId === null);
290
+
291
+ if (mappedResources.length === 0) {
292
+ return {
293
+ success: false,
294
+ message: 'No resources could be mapped to logical IDs',
295
+ unmappedCount: unmappedResources.length,
296
+ unmappedResources,
297
+ };
298
+ }
299
+
300
+ // 7. Generate import-resources.json format
301
+ const resourcesToImport = mappedResources.map((mapping) => ({
302
+ ResourceType: mapping.resourceType,
303
+ LogicalResourceId: mapping.logicalId,
304
+ ResourceIdentifier: this._getResourceIdentifier(mapping),
305
+ }));
306
+
307
+ // 8. Return result with warnings for user review
308
+ return {
309
+ success: true,
310
+ mappedCount: mappedResources.length,
311
+ unmappedCount: unmappedResources.length,
312
+ mappings: mappedResources,
313
+ unmappedResources,
314
+ resourcesToImport,
315
+ warnings: multiResourceWarnings,
316
+ buildTemplatePath,
317
+ deployedTemplatePath: 'CloudFormation (deployed)',
318
+ };
319
+ }
320
+
321
+ /**
322
+ * Check for multiple resources of same type
323
+ * Returns warnings when user needs to manually select
324
+ * @private
325
+ */
326
+ _checkForMultipleResources(mappings) {
327
+ const warnings = [];
328
+ const byType = {};
329
+
330
+ // Group by resource type
331
+ mappings.forEach((mapping) => {
332
+ if (!byType[mapping.resourceType]) {
333
+ byType[mapping.resourceType] = [];
334
+ }
335
+ byType[mapping.resourceType].push(mapping);
336
+ });
337
+
338
+ // Check for multiples
339
+ Object.entries(byType).forEach(([type, resources]) => {
340
+ if (resources.length > 1) {
341
+ const shortType = type.replace('AWS::EC2::', '');
342
+ warnings.push({
343
+ type: 'MULTIPLE_RESOURCES',
344
+ resourceType: type,
345
+ count: resources.length,
346
+ message: `Multiple ${shortType}s detected (${resources.length}). Review relationships before importing.`,
347
+ resources: resources.map((r) => ({
348
+ physicalId: r.physicalId,
349
+ logicalId: r.logicalId,
350
+ matchMethod: r.matchMethod,
351
+ confidence: r.confidence,
352
+ })),
353
+ });
354
+ }
355
+ });
356
+
357
+ return warnings;
358
+ }
359
+
360
+ /**
361
+ * Get CloudFormation resource identifier for import
362
+ * @private
363
+ */
364
+ _getResourceIdentifier(mapping) {
365
+ const { resourceType, physicalId } = mapping;
366
+
367
+ // Map resource types to their identifier format
368
+ const identifierMap = {
369
+ 'AWS::EC2::VPC': { VpcId: physicalId },
370
+ 'AWS::EC2::Subnet': { SubnetId: physicalId },
371
+ 'AWS::EC2::SecurityGroup': { GroupId: physicalId },
372
+ 'AWS::EC2::InternetGateway': { InternetGatewayId: physicalId },
373
+ 'AWS::EC2::NatGateway': { NatGatewayId: physicalId },
374
+ 'AWS::EC2::RouteTable': { RouteTableId: physicalId },
375
+ 'AWS::EC2::VPCEndpoint': { VpcEndpointId: physicalId },
376
+ };
377
+
378
+ return identifierMap[resourceType] || { Id: physicalId };
379
+ }
227
380
  }
228
381
 
229
382
  module.exports = RepairViaImportUseCase;
@@ -55,16 +55,27 @@ class RunHealthCheckUseCase {
55
55
  *
56
56
  * @param {Object} params
57
57
  * @param {StackIdentifier} params.stackIdentifier - Stack to check
58
+ * @param {Function} params.onProgress - Optional progress callback (step, message)
58
59
  * @returns {Promise<StackHealthReport>} Comprehensive health report
59
60
  */
60
- async execute({ stackIdentifier }) {
61
+ async execute({ stackIdentifier, onProgress }) {
62
+ // Helper to call progress callback if provided
63
+ const progress = (step, message) => {
64
+ if (onProgress) {
65
+ onProgress(step, message);
66
+ }
67
+ };
68
+
61
69
  // 1. Verify stack exists
70
+ progress('📋 Step 1/5:', 'Verifying stack exists...');
62
71
  await this.stackRepository.getStack(stackIdentifier);
63
72
 
64
73
  // 2. Detect stack-level drift
74
+ progress('🔍 Step 2/5:', 'Detecting stack drift...');
65
75
  const driftDetection = await this.stackRepository.detectStackDrift(stackIdentifier);
66
76
 
67
77
  // 3. Get all stack resources
78
+ progress('📊 Step 3/5:', 'Analyzing stack resources...');
68
79
  const stackResources = await this.stackRepository.listResources(stackIdentifier);
69
80
 
70
81
  // 4. Build resource entities with drift status
@@ -101,10 +112,12 @@ class RunHealthCheckUseCase {
101
112
  resourceDrift.propertyDifferences &&
102
113
  resourceDrift.propertyDifferences.length > 0
103
114
  ) {
104
- const propertyMismatches = this.mismatchAnalyzer.analyzePropertyMismatches(
105
- resourceDrift.propertyDifferences,
106
- stackResource.resourceType
107
- );
115
+ const propertyMismatches = this.mismatchAnalyzer.analyze({
116
+ expected: resourceDrift.expectedProperties,
117
+ actual: resourceDrift.actualProperties,
118
+ propertyMutability: {},
119
+ ignoreProperties: [],
120
+ });
108
121
 
109
122
  // Create issue for each property mismatch using factory method
110
123
  for (const mismatch of propertyMismatches) {
@@ -135,6 +148,7 @@ class RunHealthCheckUseCase {
135
148
  }
136
149
 
137
150
  // 5. Find orphaned resources (exist in cloud but not in stack)
151
+ progress('🔎 Step 4/5:', 'Checking for orphaned resources...');
138
152
  const orphanedResources = await this.resourceDetector.findOrphanedResources({
139
153
  stackIdentifier,
140
154
  stackResources,
@@ -162,6 +176,7 @@ class RunHealthCheckUseCase {
162
176
  }
163
177
 
164
178
  // 6. Calculate health score using domain service
179
+ progress('🧮 Step 5/5:', 'Calculating health score...');
165
180
  const healthScore = this.healthScoreCalculator.calculate({ resources, issues });
166
181
 
167
182
  // 7. Build comprehensive health report (aggregate root)
@@ -33,7 +33,7 @@ describe('RunHealthCheckUseCase', () => {
33
33
  };
34
34
 
35
35
  mockMismatchAnalyzer = {
36
- analyzePropertyMismatches: jest.fn(),
36
+ analyze: jest.fn(),
37
37
  };
38
38
 
39
39
  mockHealthScoreCalculator = {
@@ -149,7 +149,7 @@ describe('RunHealthCheckUseCase', () => {
149
149
  mutability: PropertyMutability.MUTABLE,
150
150
  });
151
151
 
152
- mockMismatchAnalyzer.analyzePropertyMismatches.mockReturnValue([propertyMismatch]);
152
+ mockMismatchAnalyzer.analyze.mockReturnValue([propertyMismatch]);
153
153
 
154
154
  mockResourceDetector.findOrphanedResources.mockResolvedValue([]);
155
155
 
@@ -323,7 +323,7 @@ describe('RunHealthCheckUseCase', () => {
323
323
  mutability: PropertyMutability.MUTABLE,
324
324
  });
325
325
 
326
- mockMismatchAnalyzer.analyzePropertyMismatches.mockReturnValue([propertyMismatch]);
326
+ mockMismatchAnalyzer.analyze.mockReturnValue([propertyMismatch]);
327
327
 
328
328
  // Mock orphaned resources
329
329
  mockResourceDetector.findOrphanedResources.mockResolvedValue([
@@ -0,0 +1,267 @@
1
+ # acme-integrations-dev Stack Drift Analysis
2
+
3
+ **Date**: 2025-10-27
4
+ **Stack**: acme-integrations-dev (us-east-1)
5
+ **Status**: 65/100 Health Score (degraded)
6
+
7
+ ## Executive Summary
8
+
9
+ The Lambda functions were **manually moved from Frigg VPC to Default VPC**, causing:
10
+ - ✅ 16 orphaned resources (3 VPCs, 10 subnets, 3 security groups) - correctly detected
11
+ - ⚠️ 32 property mismatch warnings (VPC drift on all 16 Lambda functions)
12
+
13
+ **The template expects** Lambdas to use Frigg-managed VPC `vpc-0eadd96976d29ede7`.
14
+ **But Lambdas actually use** AWS default VPC `vpc-01f21101d4ed6db59`.
15
+
16
+ ## Detailed Analysis
17
+
18
+ ### CloudFormation Template Expectations
19
+
20
+ The template specifies Lambda functions should use:
21
+
22
+ ```yaml
23
+ VpcConfig:
24
+ SecurityGroupIds:
25
+ - sg-07c01370e830b6ad6 # Frigg Lambda SG (in vpc-0eadd96976d29ede7)
26
+ SubnetIds:
27
+ - subnet-00ab9e0502e66aac3 # Private subnet 1 (in vpc-0eadd96976d29ede7)
28
+ - subnet-00d085a52937aaf91 # Private subnet 2 (in vpc-0eadd96976d29ede7)
29
+ ```
30
+
31
+ **These resources belong to:** `vpc-0eadd96976d29ede7` (10.0.0.0/16)
32
+
33
+ ### Actual Lambda Configuration
34
+
35
+ Lambda functions are actually running in:
36
+
37
+ ```json
38
+ {
39
+ "VpcId": "vpc-01f21101d4ed6db59", // AWS Default VPC (172.31.0.0/16)
40
+ "SecurityGroupIds": [
41
+ "sg-0aca40438d17344c4" // Default VPC security group (NOT in template)
42
+ ],
43
+ "SubnetIds": [
44
+ "subnet-020d32e3ca398a041", // Default VPC subnet 1 (NOT in template)
45
+ "subnet-0c186318804aba790" // Default VPC subnet 2 (NOT in template)
46
+ ]
47
+ }
48
+ ```
49
+
50
+ **These resources belong to:** `vpc-01f21101d4ed6db59` (172.31.0.0/16) - AWS Default VPC
51
+
52
+ ### Orphaned Resources Analysis
53
+
54
+ #### 1. VPC: vpc-0eadd96976d29ede7 (10.0.0.0/16) ✅ CORRECT VPC TO IMPORT
55
+
56
+ **Status:**
57
+ - Has CloudFormation tags: `stack-name=acme-integrations-dev`, `logical-id=FriggVPC`
58
+ - NOT in CloudFormation stack (stack has 0 VPCs managed)
59
+ - Contains subnets that are **EXPECTED by template**:
60
+ - `subnet-00ab9e0502e66aac3` (10.0.0.0/24) - Expected by template ✅
61
+ - `subnet-00d085a52937aaf91` (10.0.1.0/24) - Expected by template ✅
62
+ - Contains security group that is **EXPECTED by template**:
63
+ - `sg-07c01370e830b6ad6` - Expected by template ✅
64
+
65
+ **Conclusion:** This is the **CORRECT VPC** that should be imported! The template expects Lambdas to use this VPC, but they were manually moved to default VPC.
66
+
67
+ **Stage Verification:** Has `STAGE=dev` tag ✅
68
+
69
+ #### 2. VPC: vpc-0e2351eac99adcb83 (10.0.0.0/16) - OLD/DUPLICATE
70
+
71
+ **Status:**
72
+ - Has CloudFormation tags: `stack-name=acme-integrations-dev`, `logical-id=FriggVPC`
73
+ - NOT in CloudFormation stack
74
+ - Contains orphaned subnets NOT referenced by template
75
+ - Same CIDR as vpc-0eadd96976d29ede7 (duplicate)
76
+
77
+ **Conclusion:** Old/duplicate VPC, should be **DELETED**
78
+
79
+ **Stage Verification:** Has `STAGE=dev` tag
80
+
81
+ #### 3. VPC: vpc-020a0365610c05f0b (10.0.0.0/16) - OLD/DUPLICATE
82
+
83
+ **Status:**
84
+ - Has CloudFormation tags: `stack-name=acme-integrations-dev`, `logical-id=FriggVPC`
85
+ - NOT in CloudFormation stack
86
+ - Contains orphaned subnets NOT referenced by template
87
+ - Same CIDR as vpc-0eadd96976d29ede7 (duplicate)
88
+
89
+ **Conclusion:** Old/duplicate VPC, should be **DELETED**
90
+
91
+ **Stage Verification:** Has `STAGE=dev` tag
92
+
93
+ ## What Happened?
94
+
95
+ 1. **Initial Deployment**: CloudFormation created `vpc-0eadd96976d29ede7` with subnets and security groups
96
+ 2. **VPC Removed from Stack**: VPC was removed from CloudFormation management (but resources still exist)
97
+ 3. **Manual Migration**: Lambda functions were manually updated to use default VPC instead
98
+ 4. **Result**: Template expects Frigg VPC, but Lambdas use default VPC → drift
99
+
100
+ ## Recommended Actions
101
+
102
+ ### Option 1: Import Frigg VPC and Let CloudFormation Fix Drift (RECOMMENDED)
103
+
104
+ **Steps:**
105
+
106
+ 1. **Import the correct VPC and its resources:**
107
+ ```bash
108
+ # Import vpc-0eadd96976d29ede7 and its subnets/SG
109
+ frigg repair --import quo-integrations-dev
110
+ # When prompted, select ONLY vpc-0eadd96976d29ede7
111
+ ```
112
+
113
+ 2. **CloudFormation will automatically update Lambdas:**
114
+ - CloudFormation will detect the VPC/subnet/SG mismatch
115
+ - Next stack update will **automatically** update Lambda VPC configs
116
+ - Lambdas will be moved from default VPC back to Frigg VPC
117
+
118
+ 3. **Delete the duplicate VPCs:**
119
+ ```bash
120
+ aws ec2 delete-vpc --vpc-id vpc-0e2351eac99adcb83
121
+ aws ec2 delete-vpc --vpc-id vpc-020a0365610c05f0b
122
+ ```
123
+
124
+ **CloudFormation Behavior:**
125
+ - ✅ YES, CloudFormation WILL automatically update Lambda VPC configs
126
+ - ✅ CloudFormation will handle the migration safely (blue-green deployment)
127
+ - ✅ No downtime - new Lambda versions created, traffic switched over
128
+
129
+ **Benefits:**
130
+ - ✅ Stack returns to intended state (Lambdas in Frigg VPC)
131
+ - ✅ Health score improves to 100/100
132
+ - ✅ Proper VPC isolation restored
133
+ - ✅ CloudFormation manages all resources again
134
+
135
+ **Risks:**
136
+ - ⚠️ Lambda cold starts during VPC migration (~10-30 seconds)
137
+ - ⚠️ Must ensure Frigg VPC networking is configured correctly
138
+
139
+ ### Option 2: Update Template to Use Default VPC (NOT RECOMMENDED)
140
+
141
+ **Steps:**
142
+
143
+ 1. Update serverless.yml to remove VPC configuration
144
+ 2. Deploy stack update
145
+ 3. Delete all 3 orphaned Frigg VPCs
146
+
147
+ **Why NOT recommended:**
148
+ - ❌ Loses VPC isolation benefits
149
+ - ❌ Lambda functions exposed to internet (less secure)
150
+ - ❌ Shared default VPC across all accounts
151
+ - ❌ No control over networking
152
+
153
+ ### Option 3: Delete All and Let CloudFormation Recreate (RISKY)
154
+
155
+ **Steps:**
156
+
157
+ 1. Delete all 3 orphaned VPCs
158
+ 2. Add VPC back to CloudFormation template
159
+ 3. Deploy stack update
160
+
161
+ **Why RISKY:**
162
+ - ❌ CloudFormation will create NEW VPC with different ID
163
+ - ❌ Requires stack update to fix Lambda drift
164
+ - ❌ More disruptive than import
165
+
166
+ ## Answering Your Questions
167
+
168
+ ### Q1: What are the drifted properties?
169
+
170
+ **Answer:**
171
+ - **Expected** (from template): Subnets in `vpc-0eadd96976d29ede7` (Frigg VPC)
172
+ - **Actual** (in AWS): Subnets in `vpc-01f21101d4ed6db59` (default VPC)
173
+ - **Cause**: Lambdas were manually moved to default VPC
174
+
175
+ ### Q2: Will CloudFormation migrate Lambdas if we import the VPC?
176
+
177
+ **Answer:** ✅ **YES!**
178
+
179
+ When you import `vpc-0eadd96976d29ede7` and its subnets/SG:
180
+ 1. CloudFormation will recognize the resources exist
181
+ 2. Next stack update will detect Lambda VPC config drift
182
+ 3. CloudFormation will automatically update Lambda functions to use imported VPC
183
+ 4. Migration happens safely with blue-green deployment (no downtime)
184
+
185
+ ### Q3: What's the right approach?
186
+
187
+ **Answer:** **Import `vpc-0eadd96976d29ede7` and delete the other 2 VPCs**
188
+
189
+ This VPC is the one the template expects, and it contains the correct subnets/SG that match the template.
190
+
191
+ ### Q4: Are these VPCs intended for -dev stage?
192
+
193
+ **Answer:** ✅ **YES, all 3 VPCs have `STAGE=dev` tags**
194
+
195
+ But only `vpc-0eadd96976d29ede7` contains the resources referenced by the template. The other 2 are duplicates/old deployments.
196
+
197
+ ## Implementation Plan
198
+
199
+ 1. ✅ **Verify VPC networking is correct:**
200
+ ```bash
201
+ # Check route tables, NAT gateways, internet gateways
202
+ aws ec2 describe-route-tables --filters "Name=vpc-id,Values=vpc-0eadd96976d29ede7"
203
+ aws ec2 describe-nat-gateways --filter "Name=vpc-id,Values=vpc-0eadd96976d29ede7"
204
+ ```
205
+
206
+ 2. ✅ **Import the correct VPC:**
207
+ ```bash
208
+ frigg repair --import quo-integrations-dev
209
+ # Select: vpc-0eadd96976d29ede7 ONLY
210
+ # Select: All subnets in vpc-0eadd96976d29ede7
211
+ # Select: sg-07c01370e830b6ad6
212
+ ```
213
+
214
+ 3. ✅ **Deploy stack update to fix Lambda drift:**
215
+ ```bash
216
+ frigg deploy --stage dev
217
+ # CloudFormation will update Lambda VPC configs automatically
218
+ ```
219
+
220
+ 4. ✅ **Verify Lambdas migrated successfully:**
221
+ ```bash
222
+ aws lambda get-function-configuration --function-name quo-integrations-dev-attio \
223
+ --query 'VpcConfig.{VpcId:VpcId,SubnetIds:SubnetIds}'
224
+ ```
225
+
226
+ 5. ✅ **Delete duplicate VPCs:**
227
+ ```bash
228
+ # Delete subnets first, then VPCs
229
+ aws ec2 delete-vpc --vpc-id vpc-0e2351eac99adcb83
230
+ aws ec2 delete-vpc --vpc-id vpc-020a0365610c05f0b
231
+ ```
232
+
233
+ 6. ✅ **Re-run health check:**
234
+ ```bash
235
+ frigg doctor quo-integrations-dev
236
+ # Should show 100/100 health score
237
+ ```
238
+
239
+ ## Next Steps for Relationship Analysis Implementation
240
+
241
+ Based on this real-world scenario, the relationship analysis should:
242
+
243
+ 1. **Detect template-expected resources:**
244
+ - Parse CloudFormation template to find expected VPC config
245
+ - Extract subnet IDs, security group IDs from template
246
+
247
+ 2. **Match orphans against expected resources:**
248
+ - `vpc-0eadd96976d29ede7` contains expected subnets → **HIGH priority import**
249
+ - Other VPCs don't contain expected resources → **LOW priority (delete)**
250
+
251
+ 3. **Show recommendation:**
252
+ ```
253
+ ⚠ Multiple VPCs detected (3 orphaned)
254
+
255
+ Analysis:
256
+ 1. vpc-0eadd96976d29ede7 - Contains resources expected by template [IMPORT THIS]
257
+ - subnet-00ab9e0502e66aac3 (expected)
258
+ - subnet-00d085a52937aaf91 (expected)
259
+ - sg-07c01370e830b6ad6 (expected)
260
+
261
+ 2. vpc-0e2351eac99adcb83 - No expected resources [DELETE]
262
+ 3. vpc-020a0365610c05f0b - No expected resources [DELETE]
263
+
264
+ Recommendation:
265
+ ✅ Import vpc-0eadd96976d29ede7 to restore template compliance
266
+ ❌ Delete vpc-0e2351eac99adcb83 and vpc-020a0365610c05f0b (old/unused)
267
+ ```