@friggframework/devtools 2.0.0-next.45 → 2.0.0-next.46

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 (212) hide show
  1. package/infrastructure/ARCHITECTURE.md +487 -0
  2. package/infrastructure/HEALTH.md +468 -0
  3. package/infrastructure/README.md +51 -0
  4. package/infrastructure/__tests__/postgres-config.test.js +914 -0
  5. package/infrastructure/__tests__/template-generation.test.js +687 -0
  6. package/infrastructure/create-frigg-infrastructure.js +1 -1
  7. package/infrastructure/docs/POSTGRES-CONFIGURATION.md +630 -0
  8. package/infrastructure/{DEPLOYMENT-INSTRUCTIONS.md → docs/deployment-instructions.md} +3 -3
  9. package/infrastructure/{IAM-POLICY-TEMPLATES.md → docs/iam-policy-templates.md} +9 -10
  10. package/infrastructure/domains/database/aurora-builder.js +809 -0
  11. package/infrastructure/domains/database/aurora-builder.test.js +950 -0
  12. package/infrastructure/domains/database/aurora-discovery.js +87 -0
  13. package/infrastructure/domains/database/aurora-discovery.test.js +188 -0
  14. package/infrastructure/domains/database/aurora-resolver.js +210 -0
  15. package/infrastructure/domains/database/aurora-resolver.test.js +347 -0
  16. package/infrastructure/domains/database/migration-builder.js +633 -0
  17. package/infrastructure/domains/database/migration-builder.test.js +294 -0
  18. package/infrastructure/domains/database/migration-resolver.js +163 -0
  19. package/infrastructure/domains/database/migration-resolver.test.js +337 -0
  20. package/infrastructure/domains/health/application/ports/IPropertyReconciler.js +164 -0
  21. package/infrastructure/domains/health/application/ports/IResourceDetector.js +129 -0
  22. package/infrastructure/domains/health/application/ports/IResourceImporter.js +142 -0
  23. package/infrastructure/domains/health/application/ports/IStackRepository.js +131 -0
  24. package/infrastructure/domains/health/application/ports/index.js +26 -0
  25. package/infrastructure/domains/health/application/use-cases/__tests__/execute-resource-import-use-case.test.js +679 -0
  26. package/infrastructure/domains/health/application/use-cases/__tests__/mismatch-analyzer-method-name.test.js +167 -0
  27. package/infrastructure/domains/health/application/use-cases/__tests__/repair-via-import-use-case.test.js +1130 -0
  28. package/infrastructure/domains/health/application/use-cases/execute-resource-import-use-case.js +221 -0
  29. package/infrastructure/domains/health/application/use-cases/reconcile-properties-use-case.js +152 -0
  30. package/infrastructure/domains/health/application/use-cases/reconcile-properties-use-case.test.js +343 -0
  31. package/infrastructure/domains/health/application/use-cases/repair-via-import-use-case.js +535 -0
  32. package/infrastructure/domains/health/application/use-cases/repair-via-import-use-case.test.js +376 -0
  33. package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.js +213 -0
  34. package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.test.js +441 -0
  35. package/infrastructure/domains/health/docs/ACME-DEV-DRIFT-ANALYSIS.md +267 -0
  36. package/infrastructure/domains/health/docs/BUILD-VS-DEPLOYED-TEMPLATE-ANALYSIS.md +324 -0
  37. package/infrastructure/domains/health/docs/ORPHAN-DETECTION-ANALYSIS.md +386 -0
  38. package/infrastructure/domains/health/docs/SPEC-CLEANUP-COMMAND.md +1419 -0
  39. package/infrastructure/domains/health/docs/TDD-IMPLEMENTATION-SUMMARY.md +391 -0
  40. package/infrastructure/domains/health/docs/TEMPLATE-COMPARISON-IMPLEMENTATION.md +551 -0
  41. package/infrastructure/domains/health/domain/entities/issue.js +299 -0
  42. package/infrastructure/domains/health/domain/entities/issue.test.js +528 -0
  43. package/infrastructure/domains/health/domain/entities/property-mismatch.js +108 -0
  44. package/infrastructure/domains/health/domain/entities/property-mismatch.test.js +275 -0
  45. package/infrastructure/domains/health/domain/entities/resource.js +159 -0
  46. package/infrastructure/domains/health/domain/entities/resource.test.js +432 -0
  47. package/infrastructure/domains/health/domain/entities/stack-health-report.js +306 -0
  48. package/infrastructure/domains/health/domain/entities/stack-health-report.test.js +601 -0
  49. package/infrastructure/domains/health/domain/services/__tests__/health-score-percentage-based.test.js +380 -0
  50. package/infrastructure/domains/health/domain/services/__tests__/import-progress-monitor.test.js +971 -0
  51. package/infrastructure/domains/health/domain/services/__tests__/import-template-generator.test.js +1150 -0
  52. package/infrastructure/domains/health/domain/services/__tests__/logical-id-mapper.test.js +672 -0
  53. package/infrastructure/domains/health/domain/services/__tests__/template-parser.test.js +496 -0
  54. package/infrastructure/domains/health/domain/services/__tests__/update-progress-monitor.test.js +419 -0
  55. package/infrastructure/domains/health/domain/services/health-score-calculator.js +248 -0
  56. package/infrastructure/domains/health/domain/services/health-score-calculator.test.js +504 -0
  57. package/infrastructure/domains/health/domain/services/import-progress-monitor.js +195 -0
  58. package/infrastructure/domains/health/domain/services/import-template-generator.js +435 -0
  59. package/infrastructure/domains/health/domain/services/logical-id-mapper.js +345 -0
  60. package/infrastructure/domains/health/domain/services/mismatch-analyzer.js +234 -0
  61. package/infrastructure/domains/health/domain/services/mismatch-analyzer.test.js +431 -0
  62. package/infrastructure/domains/health/domain/services/property-mutability-config.js +382 -0
  63. package/infrastructure/domains/health/domain/services/template-parser.js +245 -0
  64. package/infrastructure/domains/health/domain/services/update-progress-monitor.js +192 -0
  65. package/infrastructure/domains/health/domain/value-objects/health-score.js +138 -0
  66. package/infrastructure/domains/health/domain/value-objects/health-score.test.js +267 -0
  67. package/infrastructure/domains/health/domain/value-objects/property-mutability.js +161 -0
  68. package/infrastructure/domains/health/domain/value-objects/property-mutability.test.js +198 -0
  69. package/infrastructure/domains/health/domain/value-objects/resource-state.js +167 -0
  70. package/infrastructure/domains/health/domain/value-objects/resource-state.test.js +196 -0
  71. package/infrastructure/domains/health/domain/value-objects/stack-identifier.js +192 -0
  72. package/infrastructure/domains/health/domain/value-objects/stack-identifier.test.js +262 -0
  73. package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-cfn-tagged.test.js +312 -0
  74. package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-multi-stack.test.js +367 -0
  75. package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-relationship-analysis.test.js +432 -0
  76. package/infrastructure/domains/health/infrastructure/adapters/aws-property-reconciler.js +784 -0
  77. package/infrastructure/domains/health/infrastructure/adapters/aws-property-reconciler.test.js +1133 -0
  78. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector.js +565 -0
  79. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector.test.js +554 -0
  80. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-importer.js +318 -0
  81. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-importer.test.js +398 -0
  82. package/infrastructure/domains/health/infrastructure/adapters/aws-stack-repository.js +777 -0
  83. package/infrastructure/domains/health/infrastructure/adapters/aws-stack-repository.test.js +580 -0
  84. package/infrastructure/domains/integration/integration-builder.js +397 -0
  85. package/infrastructure/domains/integration/integration-builder.test.js +593 -0
  86. package/infrastructure/domains/integration/integration-resolver.js +170 -0
  87. package/infrastructure/domains/integration/integration-resolver.test.js +369 -0
  88. package/infrastructure/domains/integration/websocket-builder.js +69 -0
  89. package/infrastructure/domains/integration/websocket-builder.test.js +195 -0
  90. package/infrastructure/domains/networking/vpc-builder.js +1829 -0
  91. package/infrastructure/domains/networking/vpc-builder.test.js +1262 -0
  92. package/infrastructure/domains/networking/vpc-discovery.js +177 -0
  93. package/infrastructure/domains/networking/vpc-discovery.test.js +350 -0
  94. package/infrastructure/domains/networking/vpc-resolver.js +324 -0
  95. package/infrastructure/domains/networking/vpc-resolver.test.js +501 -0
  96. package/infrastructure/domains/parameters/ssm-builder.js +79 -0
  97. package/infrastructure/domains/parameters/ssm-builder.test.js +189 -0
  98. package/infrastructure/domains/parameters/ssm-discovery.js +84 -0
  99. package/infrastructure/domains/parameters/ssm-discovery.test.js +210 -0
  100. package/infrastructure/{iam-generator.js → domains/security/iam-generator.js} +2 -2
  101. package/infrastructure/domains/security/kms-builder.js +366 -0
  102. package/infrastructure/domains/security/kms-builder.test.js +374 -0
  103. package/infrastructure/domains/security/kms-discovery.js +80 -0
  104. package/infrastructure/domains/security/kms-discovery.test.js +177 -0
  105. package/infrastructure/domains/security/kms-resolver.js +96 -0
  106. package/infrastructure/domains/security/kms-resolver.test.js +216 -0
  107. package/infrastructure/domains/shared/base-builder.js +112 -0
  108. package/infrastructure/domains/shared/base-resolver.js +186 -0
  109. package/infrastructure/domains/shared/base-resolver.test.js +305 -0
  110. package/infrastructure/domains/shared/builder-orchestrator.js +212 -0
  111. package/infrastructure/domains/shared/builder-orchestrator.test.js +213 -0
  112. package/infrastructure/domains/shared/cloudformation-discovery-v2.js +334 -0
  113. package/infrastructure/domains/shared/cloudformation-discovery.js +375 -0
  114. package/infrastructure/domains/shared/cloudformation-discovery.test.js +590 -0
  115. package/infrastructure/domains/shared/environment-builder.js +119 -0
  116. package/infrastructure/domains/shared/environment-builder.test.js +247 -0
  117. package/infrastructure/domains/shared/providers/aws-provider-adapter.js +544 -0
  118. package/infrastructure/domains/shared/providers/aws-provider-adapter.test.js +377 -0
  119. package/infrastructure/domains/shared/providers/azure-provider-adapter.stub.js +93 -0
  120. package/infrastructure/domains/shared/providers/cloud-provider-adapter.js +136 -0
  121. package/infrastructure/domains/shared/providers/gcp-provider-adapter.stub.js +82 -0
  122. package/infrastructure/domains/shared/providers/provider-factory.js +108 -0
  123. package/infrastructure/domains/shared/providers/provider-factory.test.js +170 -0
  124. package/infrastructure/domains/shared/resource-discovery.js +192 -0
  125. package/infrastructure/domains/shared/resource-discovery.test.js +552 -0
  126. package/infrastructure/domains/shared/types/app-definition.js +205 -0
  127. package/infrastructure/domains/shared/types/discovery-result.js +106 -0
  128. package/infrastructure/domains/shared/types/discovery-result.test.js +258 -0
  129. package/infrastructure/domains/shared/types/index.js +46 -0
  130. package/infrastructure/domains/shared/types/resource-ownership.js +108 -0
  131. package/infrastructure/domains/shared/types/resource-ownership.test.js +101 -0
  132. package/infrastructure/domains/shared/utilities/base-definition-factory.js +380 -0
  133. package/infrastructure/domains/shared/utilities/base-definition-factory.js.bak +338 -0
  134. package/infrastructure/domains/shared/utilities/base-definition-factory.test.js +248 -0
  135. package/infrastructure/domains/shared/utilities/handler-path-resolver.js +134 -0
  136. package/infrastructure/domains/shared/utilities/handler-path-resolver.test.js +268 -0
  137. package/infrastructure/domains/shared/utilities/prisma-layer-manager.js +55 -0
  138. package/infrastructure/domains/shared/utilities/prisma-layer-manager.test.js +138 -0
  139. package/infrastructure/{env-validator.js → domains/shared/validation/env-validator.js} +2 -1
  140. package/infrastructure/domains/shared/validation/env-validator.test.js +173 -0
  141. package/infrastructure/esbuild.config.js +53 -0
  142. package/infrastructure/infrastructure-composer.js +87 -0
  143. package/infrastructure/{serverless-template.test.js → infrastructure-composer.test.js} +115 -24
  144. package/infrastructure/scripts/build-prisma-layer.js +553 -0
  145. package/infrastructure/scripts/build-prisma-layer.test.js +102 -0
  146. package/infrastructure/{build-time-discovery.js → scripts/build-time-discovery.js} +80 -48
  147. package/infrastructure/{build-time-discovery.test.js → scripts/build-time-discovery.test.js} +5 -4
  148. package/layers/prisma/nodejs/package.json +8 -0
  149. package/management-ui/server/utils/cliIntegration.js +1 -1
  150. package/management-ui/server/utils/environment/awsParameterStore.js +29 -18
  151. package/package.json +11 -11
  152. package/frigg-cli/.eslintrc.js +0 -141
  153. package/frigg-cli/__tests__/unit/commands/build.test.js +0 -251
  154. package/frigg-cli/__tests__/unit/commands/db-setup.test.js +0 -548
  155. package/frigg-cli/__tests__/unit/commands/install.test.js +0 -400
  156. package/frigg-cli/__tests__/unit/commands/ui.test.js +0 -346
  157. package/frigg-cli/__tests__/unit/utils/database-validator.test.js +0 -366
  158. package/frigg-cli/__tests__/unit/utils/error-messages.test.js +0 -304
  159. package/frigg-cli/__tests__/unit/utils/prisma-runner.test.js +0 -486
  160. package/frigg-cli/__tests__/utils/mock-factory.js +0 -270
  161. package/frigg-cli/__tests__/utils/prisma-mock.js +0 -194
  162. package/frigg-cli/__tests__/utils/test-fixtures.js +0 -463
  163. package/frigg-cli/__tests__/utils/test-setup.js +0 -287
  164. package/frigg-cli/build-command/index.js +0 -65
  165. package/frigg-cli/db-setup-command/index.js +0 -193
  166. package/frigg-cli/deploy-command/index.js +0 -175
  167. package/frigg-cli/generate-command/__tests__/generate-command.test.js +0 -301
  168. package/frigg-cli/generate-command/azure-generator.js +0 -43
  169. package/frigg-cli/generate-command/gcp-generator.js +0 -47
  170. package/frigg-cli/generate-command/index.js +0 -332
  171. package/frigg-cli/generate-command/terraform-generator.js +0 -555
  172. package/frigg-cli/generate-iam-command.js +0 -118
  173. package/frigg-cli/index.js +0 -75
  174. package/frigg-cli/index.test.js +0 -158
  175. package/frigg-cli/init-command/backend-first-handler.js +0 -756
  176. package/frigg-cli/init-command/index.js +0 -93
  177. package/frigg-cli/init-command/template-handler.js +0 -143
  178. package/frigg-cli/install-command/backend-js.js +0 -33
  179. package/frigg-cli/install-command/commit-changes.js +0 -16
  180. package/frigg-cli/install-command/environment-variables.js +0 -127
  181. package/frigg-cli/install-command/environment-variables.test.js +0 -136
  182. package/frigg-cli/install-command/index.js +0 -54
  183. package/frigg-cli/install-command/install-package.js +0 -13
  184. package/frigg-cli/install-command/integration-file.js +0 -30
  185. package/frigg-cli/install-command/logger.js +0 -12
  186. package/frigg-cli/install-command/template.js +0 -90
  187. package/frigg-cli/install-command/validate-package.js +0 -75
  188. package/frigg-cli/jest.config.js +0 -124
  189. package/frigg-cli/package.json +0 -54
  190. package/frigg-cli/start-command/index.js +0 -149
  191. package/frigg-cli/start-command/start-command.test.js +0 -297
  192. package/frigg-cli/test/init-command.test.js +0 -180
  193. package/frigg-cli/test/npm-registry.test.js +0 -319
  194. package/frigg-cli/ui-command/index.js +0 -154
  195. package/frigg-cli/utils/app-resolver.js +0 -319
  196. package/frigg-cli/utils/backend-path.js +0 -25
  197. package/frigg-cli/utils/database-validator.js +0 -161
  198. package/frigg-cli/utils/error-messages.js +0 -257
  199. package/frigg-cli/utils/npm-registry.js +0 -167
  200. package/frigg-cli/utils/prisma-runner.js +0 -280
  201. package/frigg-cli/utils/process-manager.js +0 -199
  202. package/frigg-cli/utils/repo-detection.js +0 -405
  203. package/infrastructure/aws-discovery.js +0 -1176
  204. package/infrastructure/aws-discovery.test.js +0 -1220
  205. package/infrastructure/serverless-template.js +0 -2094
  206. /package/infrastructure/{WEBSOCKET-CONFIGURATION.md → docs/WEBSOCKET-CONFIGURATION.md} +0 -0
  207. /package/infrastructure/{GENERATE-IAM-DOCS.md → docs/generate-iam-command.md} +0 -0
  208. /package/infrastructure/{iam-generator.test.js → domains/security/iam-generator.test.js} +0 -0
  209. /package/infrastructure/{frigg-deployment-iam-stack.yaml → domains/security/templates/frigg-deployment-iam-stack.yaml} +0 -0
  210. /package/infrastructure/{iam-policy-basic.json → domains/security/templates/iam-policy-basic.json} +0 -0
  211. /package/infrastructure/{iam-policy-full.json → domains/security/templates/iam-policy-full.json} +0 -0
  212. /package/infrastructure/{run-discovery.js → scripts/run-discovery.js} +0 -0
@@ -0,0 +1,432 @@
1
+ /**
2
+ * TDD Test for Orphan Detection with Relationship Analysis
3
+ *
4
+ * PROBLEM: When multiple orphaned resources of the same type are detected,
5
+ * we need to help users identify which ones are actually relevant to import.
6
+ *
7
+ * Real-world example from acme-integrations-dev:
8
+ * - 3 orphaned VPCs detected (vpc-0eadd96976d29ede7, vpc-0e2351eac99adcb83, vpc-020a0365610c05f0b)
9
+ * - 10 orphaned subnets detected
10
+ * - 3 orphaned security groups detected
11
+ * - 16 Lambda functions with VPC drift (all reference same VPC/subnets/SGs)
12
+ *
13
+ * ACTUAL AWS REALITY (verified 2025-10-27):
14
+ * - Lambda functions use DEFAULT VPC: vpc-01f21101d4ed6db59 (172.31.0.0/16, no Frigg tags)
15
+ * - Lambda subnets: subnet-020d32e3ca398a041, subnet-0c186318804aba790 (in default VPC)
16
+ * - Lambda security group: sg-0aca40438d17344c4 (in default VPC)
17
+ * - 3 orphaned VPCs are ALL unused (10.0.0.0/16, all have Frigg CFN tags but not in stack)
18
+ * - CloudFormation stack has ZERO VPCs (stack doesn't manage VPC)
19
+ *
20
+ * SOLUTION: Analyze relationships between drifted resources and orphaned resources
21
+ * to identify which orphaned resources are actually being referenced.
22
+ *
23
+ * Key Insight: If 16 Lambda functions are all drifted to use:
24
+ * - VPC: vpc-01f21101d4ed6db59 (default VPC, no tags)
25
+ * - SecurityGroup: sg-0aca40438d17344c4
26
+ * - Subnets: subnet-020d32e3ca398a041, subnet-0c186318804aba790
27
+ *
28
+ * But we detect 3 orphaned VPCs with Frigg tags:
29
+ * - vpc-0eadd96976d29ede7 (10.0.0.0/16)
30
+ * - vpc-0e2351eac99adcb83 (10.0.0.0/16)
31
+ * - vpc-020a0365610c05f0b (10.0.0.0/16)
32
+ *
33
+ * Then NONE of these orphaned VPCs are actually being used! They're old unused
34
+ * resources from previous deployments that should be cleaned up, not imported.
35
+ *
36
+ * RELATIONSHIP ANALYSIS:
37
+ * 1. Extract referenced resource IDs from drift issues
38
+ * - Lambda VpcConfig.SecurityGroupIds → extract SG IDs
39
+ * - Lambda VpcConfig.SubnetIds → extract subnet IDs
40
+ * - Subnets reference VPCs
41
+ * - Security groups reference VPCs
42
+ *
43
+ * 2. Group orphaned resources by type and count
44
+ *
45
+ * 3. For each orphaned resource:
46
+ * - Check if it's referenced by any drifted resource
47
+ * - If yes, mark as "actively used" (high priority to import)
48
+ * - If no, mark as "unused orphan" (likely old/irrelevant)
49
+ *
50
+ * 4. Add relationship metadata to orphaned resources:
51
+ * - referencedBy: [list of resources that reference this orphan]
52
+ * - relatedOrphans: [other orphans in same VPC/group]
53
+ *
54
+ * 5. When multiple resources of same type exist, show warning:
55
+ * "Multiple VPCs detected. Review relationships before importing."
56
+ */
57
+
58
+ const AWSResourceDetector = require('../aws-resource-detector');
59
+ const StackIdentifier = require('../../../domain/value-objects/stack-identifier');
60
+ const Issue = require('../../../domain/entities/issue');
61
+ const PropertyMismatch = require('../../../domain/entities/property-mismatch');
62
+ const PropertyMutability = require('../../../domain/value-objects/property-mutability');
63
+
64
+ // Mock AWS SDK
65
+ jest.mock('@aws-sdk/client-ec2', () => ({
66
+ EC2Client: jest.fn(),
67
+ DescribeVpcsCommand: jest.fn(),
68
+ DescribeSubnetsCommand: jest.fn(),
69
+ DescribeSecurityGroupsCommand: jest.fn(),
70
+ DescribeRouteTablesCommand: jest.fn(),
71
+ }));
72
+
73
+ jest.mock('@aws-sdk/client-rds', () => ({
74
+ RDSClient: jest.fn(),
75
+ DescribeDBClustersCommand: jest.fn(),
76
+ }));
77
+
78
+ jest.mock('@aws-sdk/client-kms', () => ({
79
+ KMSClient: jest.fn(),
80
+ ListKeysCommand: jest.fn(),
81
+ DescribeKeyCommand: jest.fn(),
82
+ ListAliasesCommand: jest.fn(),
83
+ }));
84
+
85
+ describe('Orphan Detection with Relationship Analysis (TDD)', () => {
86
+ let detector;
87
+ let mockEC2Send;
88
+ let mockRDSSend;
89
+ let mockKMSSend;
90
+
91
+ beforeEach(() => {
92
+ jest.clearAllMocks();
93
+
94
+ // Mock EC2 client
95
+ mockEC2Send = jest.fn();
96
+ const { EC2Client } = require('@aws-sdk/client-ec2');
97
+ EC2Client.mockImplementation(() => ({ send: mockEC2Send }));
98
+
99
+ // Mock RDS client
100
+ mockRDSSend = jest.fn().mockResolvedValue({ DBClusters: [] });
101
+ const { RDSClient } = require('@aws-sdk/client-rds');
102
+ RDSClient.mockImplementation(() => ({ send: mockRDSSend }));
103
+
104
+ // Mock KMS client
105
+ mockKMSSend = jest.fn().mockResolvedValue({ Keys: [] });
106
+ const { KMSClient } = require('@aws-sdk/client-kms');
107
+ KMSClient.mockImplementation(() => ({ send: mockKMSSend }));
108
+
109
+ detector = new AWSResourceDetector({ region: 'us-east-1' });
110
+ });
111
+
112
+ describe('Real-world scenario: acme-integrations-dev multiple VPCs', () => {
113
+ test('should analyze relationships between drifted Lambdas and orphaned VPC resources', async () => {
114
+ const stackIdentifier = new StackIdentifier({
115
+ stackName: 'acme-integrations-dev',
116
+ region: 'us-east-1',
117
+ });
118
+
119
+ // Stack has 16 Lambda functions (all with VPC drift)
120
+ const stackResources = [
121
+ {
122
+ logicalId: 'AttioFunction',
123
+ physicalId: 'acme-integrations-dev-attio',
124
+ resourceType: 'AWS::Lambda::Function',
125
+ },
126
+ {
127
+ logicalId: 'AuthFunction',
128
+ physicalId: 'acme-integrations-dev-auth',
129
+ resourceType: 'AWS::Lambda::Function',
130
+ },
131
+ // ... 14 more Lambda functions
132
+ ];
133
+
134
+ // Mock EC2 responses - 3 VPCs, 10 subnets, 3 security groups
135
+ mockEC2Send.mockImplementation((command) => {
136
+ if (command.constructor.name === 'DescribeVpcsCommand') {
137
+ return Promise.resolve({
138
+ Vpcs: [
139
+ {
140
+ // VPC #1: Old VPC with Frigg tags but not in stack
141
+ VpcId: 'vpc-0eadd96976d29ede7',
142
+ CidrBlock: '10.0.0.0/16',
143
+ State: 'available',
144
+ Tags: [
145
+ {
146
+ Key: 'aws:cloudformation:stack-name',
147
+ Value: 'acme-integrations-dev',
148
+ },
149
+ { Key: 'Name', Value: 'Old VPC' },
150
+ ],
151
+ },
152
+ {
153
+ // VPC #2: Another old VPC
154
+ VpcId: 'vpc-0e2351eac99adcb83',
155
+ CidrBlock: '10.1.0.0/16',
156
+ State: 'available',
157
+ Tags: [
158
+ {
159
+ Key: 'aws:cloudformation:stack-name',
160
+ Value: 'acme-integrations-dev',
161
+ },
162
+ ],
163
+ },
164
+ {
165
+ // VPC #3: Current VPC that Lambdas actually use
166
+ VpcId: 'vpc-020a0365610c05f0b',
167
+ CidrBlock: '10.2.0.0/16',
168
+ State: 'available',
169
+ Tags: [
170
+ {
171
+ Key: 'aws:cloudformation:stack-name',
172
+ Value: 'acme-integrations-dev',
173
+ },
174
+ { Key: 'Name', Value: 'Current VPC' },
175
+ ],
176
+ },
177
+ ],
178
+ });
179
+ }
180
+
181
+ if (command.constructor.name === 'DescribeSubnetsCommand') {
182
+ return Promise.resolve({
183
+ Subnets: [
184
+ // Subnets in VPC #3 (current VPC) - these are the ones Lambdas reference
185
+ {
186
+ SubnetId: 'subnet-020d32e3ca398a041',
187
+ VpcId: 'vpc-020a0365610c05f0b',
188
+ CidrBlock: '10.2.1.0/24',
189
+ AvailabilityZone: 'us-east-1a',
190
+ State: 'available',
191
+ Tags: [
192
+ {
193
+ Key: 'aws:cloudformation:stack-name',
194
+ Value: 'acme-integrations-dev',
195
+ },
196
+ ],
197
+ },
198
+ {
199
+ SubnetId: 'subnet-0c186318804aba790',
200
+ VpcId: 'vpc-020a0365610c05f0b',
201
+ CidrBlock: '10.2.2.0/24',
202
+ AvailabilityZone: 'us-east-1b',
203
+ State: 'available',
204
+ Tags: [
205
+ {
206
+ Key: 'aws:cloudformation:stack-name',
207
+ Value: 'acme-integrations-dev',
208
+ },
209
+ ],
210
+ },
211
+ // Subnets in old VPCs (unused)
212
+ {
213
+ SubnetId: 'subnet-0ad31b5ee6814b8fa',
214
+ VpcId: 'vpc-0eadd96976d29ede7',
215
+ CidrBlock: '10.0.1.0/24',
216
+ AvailabilityZone: 'us-east-1a',
217
+ State: 'available',
218
+ Tags: [
219
+ {
220
+ Key: 'aws:cloudformation:stack-name',
221
+ Value: 'acme-integrations-dev',
222
+ },
223
+ ],
224
+ },
225
+ // ... 7 more unused subnets
226
+ ],
227
+ });
228
+ }
229
+
230
+ if (command.constructor.name === 'DescribeSecurityGroupsCommand') {
231
+ return Promise.resolve({
232
+ SecurityGroups: [
233
+ // SG in current VPC (unused orphan - Lambdas use different SG)
234
+ {
235
+ GroupId: 'sg-07c01370e830b6ad6',
236
+ GroupName: 'default',
237
+ VpcId: 'vpc-020a0365610c05f0b',
238
+ Tags: [
239
+ {
240
+ Key: 'aws:cloudformation:stack-name',
241
+ Value: 'acme-integrations-dev',
242
+ },
243
+ ],
244
+ },
245
+ // SGs in old VPCs (unused)
246
+ {
247
+ GroupId: 'sg-03abddb7fb50aeaff',
248
+ GroupName: 'default',
249
+ VpcId: 'vpc-0eadd96976d29ede7',
250
+ Tags: [
251
+ {
252
+ Key: 'aws:cloudformation:stack-name',
253
+ Value: 'acme-integrations-dev',
254
+ },
255
+ ],
256
+ },
257
+ {
258
+ GroupId: 'sg-027f44ad46727df93',
259
+ GroupName: 'default',
260
+ VpcId: 'vpc-0e2351eac99adcb83',
261
+ Tags: [
262
+ {
263
+ Key: 'aws:cloudformation:stack-name',
264
+ Value: 'acme-integrations-dev',
265
+ },
266
+ ],
267
+ },
268
+ ],
269
+ });
270
+ }
271
+
272
+ return Promise.resolve({});
273
+ });
274
+
275
+ // Drift issues: Lambda functions reference subnets in current VPC
276
+ const driftIssues = [
277
+ Issue.propertyMismatch({
278
+ resourceType: 'AWS::Lambda::Function',
279
+ resourceId: 'acme-integrations-dev-attio',
280
+ mismatch: new PropertyMismatch({
281
+ propertyPath: 'VpcConfig.SubnetIds',
282
+ expectedValue: 'subnet-00ab9e0502e66aac3,subnet-00d085a52937aaf91',
283
+ actualValue: 'subnet-020d32e3ca398a041,subnet-0c186318804aba790', // ← References current VPC subnets
284
+ mutability: PropertyMutability.MUTABLE,
285
+ }),
286
+ }),
287
+ Issue.propertyMismatch({
288
+ resourceType: 'AWS::Lambda::Function',
289
+ resourceId: 'acme-integrations-dev-auth',
290
+ mismatch: new PropertyMismatch({
291
+ propertyPath: 'VpcConfig.SubnetIds',
292
+ expectedValue: 'subnet-00ab9e0502e66aac3,subnet-00d085a52937aaf91',
293
+ actualValue: 'subnet-020d32e3ca398a041,subnet-0c186318804aba790', // ← Same subnets
294
+ mutability: PropertyMutability.MUTABLE,
295
+ }),
296
+ }),
297
+ // ... 14 more Lambda functions with same subnet drift
298
+ ];
299
+
300
+ // Act
301
+ const orphans = await detector.findOrphanedResourcesWithRelationships({
302
+ stackIdentifier,
303
+ stackResources,
304
+ driftIssues,
305
+ });
306
+
307
+ // Assert: Should identify relationship metadata
308
+ expect(orphans.length).toBeGreaterThan(0);
309
+
310
+ // Find the orphaned subnets that are actually referenced
311
+ const referencedSubnets = orphans.filter(
312
+ (o) =>
313
+ o.resourceType === 'AWS::EC2::Subnet' &&
314
+ (o.physicalId === 'subnet-020d32e3ca398a041' ||
315
+ o.physicalId === 'subnet-0c186318804aba790')
316
+ );
317
+
318
+ // These subnets should be marked as "actively used"
319
+ for (const subnet of referencedSubnets) {
320
+ expect(subnet.metadata).toHaveProperty('referencedBy');
321
+ expect(subnet.metadata.referencedBy.length).toBeGreaterThan(0);
322
+ expect(subnet.metadata.isActivelyUsed).toBe(true);
323
+ }
324
+
325
+ // Find the VPC that contains these subnets
326
+ const currentVpc = orphans.find((o) => o.physicalId === 'vpc-020a0365610c05f0b');
327
+
328
+ if (currentVpc) {
329
+ expect(currentVpc.metadata).toHaveProperty('containsReferencedResources');
330
+ expect(currentVpc.metadata.containsReferencedResources).toBe(true);
331
+ }
332
+
333
+ // Old VPCs should NOT be marked as actively used
334
+ const oldVpc1 = orphans.find((o) => o.physicalId === 'vpc-0eadd96976d29ede7');
335
+ if (oldVpc1) {
336
+ expect(oldVpc1.metadata?.isActivelyUsed).toBeFalsy();
337
+ }
338
+ });
339
+
340
+ test('should flag multiple resources of same type for manual review', async () => {
341
+ const stackIdentifier = new StackIdentifier({
342
+ stackName: 'acme-integrations-dev',
343
+ region: 'us-east-1',
344
+ });
345
+
346
+ const stackResources = [
347
+ {
348
+ logicalId: 'MyLambda',
349
+ physicalId: 'my-lambda',
350
+ resourceType: 'AWS::Lambda::Function',
351
+ },
352
+ ];
353
+
354
+ // Mock 3 orphaned VPCs
355
+ mockEC2Send.mockResolvedValue({
356
+ Vpcs: [
357
+ {
358
+ VpcId: 'vpc-1',
359
+ CidrBlock: '10.0.0.0/16',
360
+ State: 'available',
361
+ Tags: [{ Key: 'aws:cloudformation:stack-name', Value: 'acme-integrations-dev' }],
362
+ },
363
+ {
364
+ VpcId: 'vpc-2',
365
+ CidrBlock: '10.1.0.0/16',
366
+ State: 'available',
367
+ Tags: [{ Key: 'aws:cloudformation:stack-name', Value: 'acme-integrations-dev' }],
368
+ },
369
+ {
370
+ VpcId: 'vpc-3',
371
+ CidrBlock: '10.2.0.0/16',
372
+ State: 'available',
373
+ Tags: [{ Key: 'aws:cloudformation:stack-name', Value: 'acme-integrations-dev' }],
374
+ },
375
+ ],
376
+ });
377
+
378
+ const result = await detector.findOrphanedResourcesWithRelationships({
379
+ stackIdentifier,
380
+ stackResources,
381
+ driftIssues: [],
382
+ });
383
+
384
+ // Should include warning metadata
385
+ const summary = detector.analyzeOrphanSummary(result);
386
+
387
+ expect(summary.warnings).toContain(
388
+ 'Multiple VPCs detected (3). Review relationships before importing.'
389
+ );
390
+ expect(summary.multipleResourceTypes).toContain('AWS::EC2::VPC');
391
+ });
392
+ });
393
+
394
+ describe('Helper method: extractReferencedResourceIds', () => {
395
+ test('should extract subnet IDs from VpcConfig.SubnetIds drift', () => {
396
+ const driftIssue = Issue.propertyMismatch({
397
+ resourceType: 'AWS::Lambda::Function',
398
+ resourceId: 'my-lambda',
399
+ mismatch: new PropertyMismatch({
400
+ propertyPath: 'VpcConfig.SubnetIds',
401
+ expectedValue: 'subnet-old-1,subnet-old-2',
402
+ actualValue: 'subnet-020d32e3ca398a041,subnet-0c186318804aba790',
403
+ mutability: PropertyMutability.MUTABLE,
404
+ }),
405
+ });
406
+
407
+ const detector = new AWSResourceDetector({ region: 'us-east-1' });
408
+ const referenced = detector._extractReferencedResourceIds([driftIssue]);
409
+
410
+ expect(referenced.subnetIds).toContain('subnet-020d32e3ca398a041');
411
+ expect(referenced.subnetIds).toContain('subnet-0c186318804aba790');
412
+ });
413
+
414
+ test('should extract security group IDs from VpcConfig.SecurityGroupIds drift', () => {
415
+ const driftIssue = Issue.propertyMismatch({
416
+ resourceType: 'AWS::Lambda::Function',
417
+ resourceId: 'my-lambda',
418
+ mismatch: new PropertyMismatch({
419
+ propertyPath: 'VpcConfig.SecurityGroupIds',
420
+ expectedValue: 'sg-old',
421
+ actualValue: 'sg-0aca40438d17344c4',
422
+ mutability: PropertyMutability.MUTABLE,
423
+ }),
424
+ });
425
+
426
+ const detector = new AWSResourceDetector({ region: 'us-east-1' });
427
+ const referenced = detector._extractReferencedResourceIds([driftIssue]);
428
+
429
+ expect(referenced.securityGroupIds).toContain('sg-0aca40438d17344c4');
430
+ });
431
+ });
432
+ });