@friggframework/devtools 2.0.0-next.45 → 2.0.0-next.47
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/infrastructure/ARCHITECTURE.md +487 -0
- package/infrastructure/HEALTH.md +468 -0
- package/infrastructure/README.md +51 -0
- package/infrastructure/__tests__/postgres-config.test.js +914 -0
- package/infrastructure/__tests__/template-generation.test.js +687 -0
- package/infrastructure/create-frigg-infrastructure.js +1 -1
- package/infrastructure/docs/POSTGRES-CONFIGURATION.md +630 -0
- package/infrastructure/{DEPLOYMENT-INSTRUCTIONS.md → docs/deployment-instructions.md} +3 -3
- package/infrastructure/{IAM-POLICY-TEMPLATES.md → docs/iam-policy-templates.md} +9 -10
- package/infrastructure/domains/database/aurora-builder.js +809 -0
- package/infrastructure/domains/database/aurora-builder.test.js +950 -0
- package/infrastructure/domains/database/aurora-discovery.js +87 -0
- package/infrastructure/domains/database/aurora-discovery.test.js +188 -0
- package/infrastructure/domains/database/aurora-resolver.js +210 -0
- package/infrastructure/domains/database/aurora-resolver.test.js +347 -0
- package/infrastructure/domains/database/migration-builder.js +695 -0
- package/infrastructure/domains/database/migration-builder.test.js +294 -0
- package/infrastructure/domains/database/migration-resolver.js +163 -0
- package/infrastructure/domains/database/migration-resolver.test.js +337 -0
- package/infrastructure/domains/health/application/ports/IPropertyReconciler.js +164 -0
- package/infrastructure/domains/health/application/ports/IResourceDetector.js +129 -0
- package/infrastructure/domains/health/application/ports/IResourceImporter.js +142 -0
- package/infrastructure/domains/health/application/ports/IStackRepository.js +131 -0
- package/infrastructure/domains/health/application/ports/index.js +26 -0
- package/infrastructure/domains/health/application/use-cases/__tests__/execute-resource-import-use-case.test.js +679 -0
- package/infrastructure/domains/health/application/use-cases/__tests__/mismatch-analyzer-method-name.test.js +167 -0
- package/infrastructure/domains/health/application/use-cases/__tests__/repair-via-import-use-case.test.js +1130 -0
- package/infrastructure/domains/health/application/use-cases/execute-resource-import-use-case.js +221 -0
- package/infrastructure/domains/health/application/use-cases/reconcile-properties-use-case.js +152 -0
- package/infrastructure/domains/health/application/use-cases/reconcile-properties-use-case.test.js +343 -0
- package/infrastructure/domains/health/application/use-cases/repair-via-import-use-case.js +535 -0
- package/infrastructure/domains/health/application/use-cases/repair-via-import-use-case.test.js +376 -0
- package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.js +213 -0
- package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.test.js +441 -0
- package/infrastructure/domains/health/docs/ACME-DEV-DRIFT-ANALYSIS.md +267 -0
- package/infrastructure/domains/health/docs/BUILD-VS-DEPLOYED-TEMPLATE-ANALYSIS.md +324 -0
- package/infrastructure/domains/health/docs/ORPHAN-DETECTION-ANALYSIS.md +386 -0
- package/infrastructure/domains/health/docs/SPEC-CLEANUP-COMMAND.md +1419 -0
- package/infrastructure/domains/health/docs/TDD-IMPLEMENTATION-SUMMARY.md +391 -0
- package/infrastructure/domains/health/docs/TEMPLATE-COMPARISON-IMPLEMENTATION.md +551 -0
- package/infrastructure/domains/health/domain/entities/issue.js +299 -0
- package/infrastructure/domains/health/domain/entities/issue.test.js +528 -0
- package/infrastructure/domains/health/domain/entities/property-mismatch.js +108 -0
- package/infrastructure/domains/health/domain/entities/property-mismatch.test.js +275 -0
- package/infrastructure/domains/health/domain/entities/resource.js +159 -0
- package/infrastructure/domains/health/domain/entities/resource.test.js +432 -0
- package/infrastructure/domains/health/domain/entities/stack-health-report.js +306 -0
- package/infrastructure/domains/health/domain/entities/stack-health-report.test.js +601 -0
- package/infrastructure/domains/health/domain/services/__tests__/health-score-percentage-based.test.js +380 -0
- package/infrastructure/domains/health/domain/services/__tests__/import-progress-monitor.test.js +971 -0
- package/infrastructure/domains/health/domain/services/__tests__/import-template-generator.test.js +1150 -0
- package/infrastructure/domains/health/domain/services/__tests__/logical-id-mapper.test.js +672 -0
- package/infrastructure/domains/health/domain/services/__tests__/template-parser.test.js +496 -0
- package/infrastructure/domains/health/domain/services/__tests__/update-progress-monitor.test.js +419 -0
- package/infrastructure/domains/health/domain/services/health-score-calculator.js +248 -0
- package/infrastructure/domains/health/domain/services/health-score-calculator.test.js +504 -0
- package/infrastructure/domains/health/domain/services/import-progress-monitor.js +195 -0
- package/infrastructure/domains/health/domain/services/import-template-generator.js +435 -0
- package/infrastructure/domains/health/domain/services/logical-id-mapper.js +345 -0
- package/infrastructure/domains/health/domain/services/mismatch-analyzer.js +234 -0
- package/infrastructure/domains/health/domain/services/mismatch-analyzer.test.js +431 -0
- package/infrastructure/domains/health/domain/services/property-mutability-config.js +382 -0
- package/infrastructure/domains/health/domain/services/template-parser.js +245 -0
- package/infrastructure/domains/health/domain/services/update-progress-monitor.js +192 -0
- package/infrastructure/domains/health/domain/value-objects/health-score.js +138 -0
- package/infrastructure/domains/health/domain/value-objects/health-score.test.js +267 -0
- package/infrastructure/domains/health/domain/value-objects/property-mutability.js +161 -0
- package/infrastructure/domains/health/domain/value-objects/property-mutability.test.js +198 -0
- package/infrastructure/domains/health/domain/value-objects/resource-state.js +167 -0
- package/infrastructure/domains/health/domain/value-objects/resource-state.test.js +196 -0
- package/infrastructure/domains/health/domain/value-objects/stack-identifier.js +192 -0
- package/infrastructure/domains/health/domain/value-objects/stack-identifier.test.js +262 -0
- package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-cfn-tagged.test.js +312 -0
- package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-multi-stack.test.js +367 -0
- package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-relationship-analysis.test.js +432 -0
- package/infrastructure/domains/health/infrastructure/adapters/aws-property-reconciler.js +784 -0
- package/infrastructure/domains/health/infrastructure/adapters/aws-property-reconciler.test.js +1133 -0
- package/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector.js +565 -0
- package/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector.test.js +554 -0
- package/infrastructure/domains/health/infrastructure/adapters/aws-resource-importer.js +318 -0
- package/infrastructure/domains/health/infrastructure/adapters/aws-resource-importer.test.js +398 -0
- package/infrastructure/domains/health/infrastructure/adapters/aws-stack-repository.js +777 -0
- package/infrastructure/domains/health/infrastructure/adapters/aws-stack-repository.test.js +580 -0
- package/infrastructure/domains/integration/integration-builder.js +397 -0
- package/infrastructure/domains/integration/integration-builder.test.js +593 -0
- package/infrastructure/domains/integration/integration-resolver.js +170 -0
- package/infrastructure/domains/integration/integration-resolver.test.js +369 -0
- package/infrastructure/domains/integration/websocket-builder.js +69 -0
- package/infrastructure/domains/integration/websocket-builder.test.js +195 -0
- package/infrastructure/domains/networking/vpc-builder.js +1829 -0
- package/infrastructure/domains/networking/vpc-builder.test.js +1262 -0
- package/infrastructure/domains/networking/vpc-discovery.js +177 -0
- package/infrastructure/domains/networking/vpc-discovery.test.js +350 -0
- package/infrastructure/domains/networking/vpc-resolver.js +324 -0
- package/infrastructure/domains/networking/vpc-resolver.test.js +501 -0
- package/infrastructure/domains/parameters/ssm-builder.js +79 -0
- package/infrastructure/domains/parameters/ssm-builder.test.js +189 -0
- package/infrastructure/domains/parameters/ssm-discovery.js +84 -0
- package/infrastructure/domains/parameters/ssm-discovery.test.js +210 -0
- package/infrastructure/{iam-generator.js → domains/security/iam-generator.js} +2 -2
- package/infrastructure/domains/security/kms-builder.js +366 -0
- package/infrastructure/domains/security/kms-builder.test.js +374 -0
- package/infrastructure/domains/security/kms-discovery.js +80 -0
- package/infrastructure/domains/security/kms-discovery.test.js +177 -0
- package/infrastructure/domains/security/kms-resolver.js +96 -0
- package/infrastructure/domains/security/kms-resolver.test.js +216 -0
- package/infrastructure/domains/shared/base-builder.js +112 -0
- package/infrastructure/domains/shared/base-resolver.js +186 -0
- package/infrastructure/domains/shared/base-resolver.test.js +305 -0
- package/infrastructure/domains/shared/builder-orchestrator.js +212 -0
- package/infrastructure/domains/shared/builder-orchestrator.test.js +213 -0
- package/infrastructure/domains/shared/cloudformation-discovery-v2.js +334 -0
- package/infrastructure/domains/shared/cloudformation-discovery.js +375 -0
- package/infrastructure/domains/shared/cloudformation-discovery.test.js +590 -0
- package/infrastructure/domains/shared/environment-builder.js +119 -0
- package/infrastructure/domains/shared/environment-builder.test.js +247 -0
- package/infrastructure/domains/shared/providers/aws-provider-adapter.js +544 -0
- package/infrastructure/domains/shared/providers/aws-provider-adapter.test.js +377 -0
- package/infrastructure/domains/shared/providers/azure-provider-adapter.stub.js +93 -0
- package/infrastructure/domains/shared/providers/cloud-provider-adapter.js +136 -0
- package/infrastructure/domains/shared/providers/gcp-provider-adapter.stub.js +82 -0
- package/infrastructure/domains/shared/providers/provider-factory.js +108 -0
- package/infrastructure/domains/shared/providers/provider-factory.test.js +170 -0
- package/infrastructure/domains/shared/resource-discovery.js +192 -0
- package/infrastructure/domains/shared/resource-discovery.test.js +552 -0
- package/infrastructure/domains/shared/types/app-definition.js +205 -0
- package/infrastructure/domains/shared/types/discovery-result.js +106 -0
- package/infrastructure/domains/shared/types/discovery-result.test.js +258 -0
- package/infrastructure/domains/shared/types/index.js +46 -0
- package/infrastructure/domains/shared/types/resource-ownership.js +108 -0
- package/infrastructure/domains/shared/types/resource-ownership.test.js +101 -0
- package/infrastructure/domains/shared/utilities/base-definition-factory.js +380 -0
- package/infrastructure/domains/shared/utilities/base-definition-factory.js.bak +338 -0
- package/infrastructure/domains/shared/utilities/base-definition-factory.test.js +248 -0
- package/infrastructure/domains/shared/utilities/handler-path-resolver.js +134 -0
- package/infrastructure/domains/shared/utilities/handler-path-resolver.test.js +268 -0
- package/infrastructure/domains/shared/utilities/prisma-layer-manager.js +55 -0
- package/infrastructure/domains/shared/utilities/prisma-layer-manager.test.js +138 -0
- package/infrastructure/{env-validator.js → domains/shared/validation/env-validator.js} +2 -1
- package/infrastructure/domains/shared/validation/env-validator.test.js +173 -0
- package/infrastructure/esbuild.config.js +53 -0
- package/infrastructure/infrastructure-composer.js +87 -0
- package/infrastructure/{serverless-template.test.js → infrastructure-composer.test.js} +115 -24
- package/infrastructure/scripts/build-prisma-layer.js +553 -0
- package/infrastructure/scripts/build-prisma-layer.test.js +102 -0
- package/infrastructure/{build-time-discovery.js → scripts/build-time-discovery.js} +80 -48
- package/infrastructure/{build-time-discovery.test.js → scripts/build-time-discovery.test.js} +5 -4
- package/layers/prisma/nodejs/package.json +8 -0
- package/management-ui/server/utils/cliIntegration.js +1 -1
- package/management-ui/server/utils/environment/awsParameterStore.js +29 -18
- package/package.json +11 -11
- package/frigg-cli/.eslintrc.js +0 -141
- package/frigg-cli/__tests__/unit/commands/build.test.js +0 -251
- package/frigg-cli/__tests__/unit/commands/db-setup.test.js +0 -548
- package/frigg-cli/__tests__/unit/commands/install.test.js +0 -400
- package/frigg-cli/__tests__/unit/commands/ui.test.js +0 -346
- package/frigg-cli/__tests__/unit/utils/database-validator.test.js +0 -366
- package/frigg-cli/__tests__/unit/utils/error-messages.test.js +0 -304
- package/frigg-cli/__tests__/unit/utils/prisma-runner.test.js +0 -486
- package/frigg-cli/__tests__/utils/mock-factory.js +0 -270
- package/frigg-cli/__tests__/utils/prisma-mock.js +0 -194
- package/frigg-cli/__tests__/utils/test-fixtures.js +0 -463
- package/frigg-cli/__tests__/utils/test-setup.js +0 -287
- package/frigg-cli/build-command/index.js +0 -65
- package/frigg-cli/db-setup-command/index.js +0 -193
- package/frigg-cli/deploy-command/index.js +0 -175
- package/frigg-cli/generate-command/__tests__/generate-command.test.js +0 -301
- package/frigg-cli/generate-command/azure-generator.js +0 -43
- package/frigg-cli/generate-command/gcp-generator.js +0 -47
- package/frigg-cli/generate-command/index.js +0 -332
- package/frigg-cli/generate-command/terraform-generator.js +0 -555
- package/frigg-cli/generate-iam-command.js +0 -118
- package/frigg-cli/index.js +0 -75
- package/frigg-cli/index.test.js +0 -158
- package/frigg-cli/init-command/backend-first-handler.js +0 -756
- package/frigg-cli/init-command/index.js +0 -93
- package/frigg-cli/init-command/template-handler.js +0 -143
- package/frigg-cli/install-command/backend-js.js +0 -33
- package/frigg-cli/install-command/commit-changes.js +0 -16
- package/frigg-cli/install-command/environment-variables.js +0 -127
- package/frigg-cli/install-command/environment-variables.test.js +0 -136
- package/frigg-cli/install-command/index.js +0 -54
- package/frigg-cli/install-command/install-package.js +0 -13
- package/frigg-cli/install-command/integration-file.js +0 -30
- package/frigg-cli/install-command/logger.js +0 -12
- package/frigg-cli/install-command/template.js +0 -90
- package/frigg-cli/install-command/validate-package.js +0 -75
- package/frigg-cli/jest.config.js +0 -124
- package/frigg-cli/package.json +0 -54
- package/frigg-cli/start-command/index.js +0 -149
- package/frigg-cli/start-command/start-command.test.js +0 -297
- package/frigg-cli/test/init-command.test.js +0 -180
- package/frigg-cli/test/npm-registry.test.js +0 -319
- package/frigg-cli/ui-command/index.js +0 -154
- package/frigg-cli/utils/app-resolver.js +0 -319
- package/frigg-cli/utils/backend-path.js +0 -25
- package/frigg-cli/utils/database-validator.js +0 -161
- package/frigg-cli/utils/error-messages.js +0 -257
- package/frigg-cli/utils/npm-registry.js +0 -167
- package/frigg-cli/utils/prisma-runner.js +0 -280
- package/frigg-cli/utils/process-manager.js +0 -199
- package/frigg-cli/utils/repo-detection.js +0 -405
- package/infrastructure/aws-discovery.js +0 -1176
- package/infrastructure/aws-discovery.test.js +0 -1220
- package/infrastructure/serverless-template.js +0 -2094
- /package/infrastructure/{WEBSOCKET-CONFIGURATION.md → docs/WEBSOCKET-CONFIGURATION.md} +0 -0
- /package/infrastructure/{GENERATE-IAM-DOCS.md → docs/generate-iam-command.md} +0 -0
- /package/infrastructure/{iam-generator.test.js → domains/security/iam-generator.test.js} +0 -0
- /package/infrastructure/{frigg-deployment-iam-stack.yaml → domains/security/templates/frigg-deployment-iam-stack.yaml} +0 -0
- /package/infrastructure/{iam-policy-basic.json → domains/security/templates/iam-policy-basic.json} +0 -0
- /package/infrastructure/{iam-policy-full.json → domains/security/templates/iam-policy-full.json} +0 -0
- /package/infrastructure/{run-discovery.js → scripts/run-discovery.js} +0 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TDD Test for Orphan Detection with CloudFormation Tags
|
|
3
|
+
*
|
|
4
|
+
* BUG DISCOVERED: Resources can have aws:cloudformation:stack-name tags
|
|
5
|
+
* but NOT actually be in the CloudFormation stack. This happens when:
|
|
6
|
+
* 1. Resources are manually created and tagged with CloudFormation tags
|
|
7
|
+
* 2. Resources are removed from stack but tags remain
|
|
8
|
+
* 3. Resources are imported but import fails/reverts
|
|
9
|
+
*
|
|
10
|
+
* Real-world example from quo-integrations-dev:
|
|
11
|
+
* - 2 VPCs both tagged with aws:cloudformation:stack-name=quo-integrations-dev
|
|
12
|
+
* - CloudFormation stack has 0 VPCs in it (verified via DescribeStackResources)
|
|
13
|
+
* - Both VPCs are orphans despite having CloudFormation tags
|
|
14
|
+
*
|
|
15
|
+
* SOLUTION: Don't trust CloudFormation tags. Instead:
|
|
16
|
+
* 1. Get actual stack resources from CloudFormation (we already have this!)
|
|
17
|
+
* 2. Build Set of physical IDs that are IN the stack
|
|
18
|
+
* 3. Compare discovered resources against this Set
|
|
19
|
+
* 4. If resource has CFN tag but physical ID not in Set = ORPHAN
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const AWSResourceDetector = require('../aws-resource-detector');
|
|
23
|
+
const StackIdentifier = require('../../../domain/value-objects/stack-identifier');
|
|
24
|
+
|
|
25
|
+
// Mock AWS SDK
|
|
26
|
+
jest.mock('@aws-sdk/client-ec2', () => ({
|
|
27
|
+
EC2Client: jest.fn(),
|
|
28
|
+
DescribeVpcsCommand: jest.fn(),
|
|
29
|
+
DescribeSubnetsCommand: jest.fn(),
|
|
30
|
+
DescribeSecurityGroupsCommand: jest.fn(),
|
|
31
|
+
DescribeRouteTablesCommand: jest.fn(),
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
jest.mock('@aws-sdk/client-rds', () => ({
|
|
35
|
+
RDSClient: jest.fn(),
|
|
36
|
+
DescribeDBClustersCommand: jest.fn(),
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
jest.mock('@aws-sdk/client-kms', () => ({
|
|
40
|
+
KMSClient: jest.fn(),
|
|
41
|
+
ListKeysCommand: jest.fn(),
|
|
42
|
+
DescribeKeyCommand: jest.fn(),
|
|
43
|
+
ListAliasesCommand: jest.fn(),
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
describe('Orphan Detection with CloudFormation-Tagged Resources (TDD)', () => {
|
|
47
|
+
let detector;
|
|
48
|
+
let mockEC2Send;
|
|
49
|
+
let mockRDSSend;
|
|
50
|
+
let mockKMSSend;
|
|
51
|
+
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
jest.clearAllMocks();
|
|
54
|
+
|
|
55
|
+
// Mock EC2 client
|
|
56
|
+
mockEC2Send = jest.fn();
|
|
57
|
+
const { EC2Client } = require('@aws-sdk/client-ec2');
|
|
58
|
+
EC2Client.mockImplementation(() => ({ send: mockEC2Send }));
|
|
59
|
+
|
|
60
|
+
// Mock RDS client - return empty arrays by default
|
|
61
|
+
mockRDSSend = jest.fn().mockResolvedValue({ DBClusters: [] });
|
|
62
|
+
const { RDSClient } = require('@aws-sdk/client-rds');
|
|
63
|
+
RDSClient.mockImplementation(() => ({ send: mockRDSSend }));
|
|
64
|
+
|
|
65
|
+
// Mock KMS client - return empty arrays by default
|
|
66
|
+
mockKMSSend = jest.fn().mockResolvedValue({ Keys: [] });
|
|
67
|
+
const { KMSClient } = require('@aws-sdk/client-kms');
|
|
68
|
+
KMSClient.mockImplementation(() => ({ send: mockKMSSend }));
|
|
69
|
+
|
|
70
|
+
detector = new AWSResourceDetector({ region: 'us-east-1' });
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('Bug: Resources with CloudFormation tags but not in stack', () => {
|
|
74
|
+
test('should detect VPCs with CloudFormation tags that are NOT in the actual stack', async () => {
|
|
75
|
+
// Real-world scenario from quo-integrations-dev
|
|
76
|
+
const stackIdentifier = new StackIdentifier({
|
|
77
|
+
stackName: 'quo-integrations-dev',
|
|
78
|
+
region: 'us-east-1',
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// CloudFormation stack has 0 VPCs (stack uses existing VPC, doesn't manage it)
|
|
82
|
+
const stackResources = [
|
|
83
|
+
{
|
|
84
|
+
logicalId: 'MyLambda',
|
|
85
|
+
physicalId: 'quo-integrations-dev-lambda',
|
|
86
|
+
resourceType: 'AWS::Lambda::Function',
|
|
87
|
+
},
|
|
88
|
+
// NO VPCs in the stack!
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
// Mock EC2 returns 2 VPCs both with CloudFormation tags
|
|
92
|
+
mockEC2Send.mockResolvedValue({
|
|
93
|
+
Vpcs: [
|
|
94
|
+
{
|
|
95
|
+
// VPC #1: Has CloudFormation tag for this stack BUT not in stack = ORPHAN
|
|
96
|
+
VpcId: 'vpc-0eadd96976d29ede7',
|
|
97
|
+
CidrBlock: '10.0.0.0/16',
|
|
98
|
+
State: 'available',
|
|
99
|
+
IsDefault: false,
|
|
100
|
+
Tags: [
|
|
101
|
+
{
|
|
102
|
+
Key: 'aws:cloudformation:stack-name',
|
|
103
|
+
Value: 'quo-integrations-dev',
|
|
104
|
+
},
|
|
105
|
+
{ Key: 'aws:cloudformation:stack-id', Value: 'arn:aws:cloudformation:...' },
|
|
106
|
+
{ Key: 'aws:cloudformation:logical-id', Value: 'FriggVPC' },
|
|
107
|
+
{ Key: 'Name', Value: 'quo-integrations-dev-vpc' },
|
|
108
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
109
|
+
],
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
// VPC #2: Has CloudFormation tag for this stack BUT not in stack = ORPHAN
|
|
113
|
+
VpcId: 'vpc-0e2351eac99adcb83',
|
|
114
|
+
CidrBlock: '10.1.0.0/16',
|
|
115
|
+
State: 'available',
|
|
116
|
+
IsDefault: false,
|
|
117
|
+
Tags: [
|
|
118
|
+
{
|
|
119
|
+
Key: 'aws:cloudformation:stack-name',
|
|
120
|
+
Value: 'quo-integrations-dev',
|
|
121
|
+
},
|
|
122
|
+
{ Key: 'aws:cloudformation:stack-id', Value: 'arn:aws:cloudformation:...' },
|
|
123
|
+
{ Key: 'aws:cloudformation:logical-id', Value: 'FriggVPC' },
|
|
124
|
+
{ Key: 'Name', Value: 'quo-integrations-dev-vpc' },
|
|
125
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
126
|
+
],
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
// VPC #3: From different stack - NOT orphaned
|
|
130
|
+
VpcId: 'vpc-other-stack',
|
|
131
|
+
CidrBlock: '10.2.0.0/16',
|
|
132
|
+
State: 'available',
|
|
133
|
+
Tags: [
|
|
134
|
+
{
|
|
135
|
+
Key: 'aws:cloudformation:stack-name',
|
|
136
|
+
Value: 'quo-integrations-prod',
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Act
|
|
144
|
+
const orphans = await detector.findOrphanedResources({
|
|
145
|
+
stackIdentifier,
|
|
146
|
+
stackResources,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Assert
|
|
150
|
+
// Should find BOTH VPCs as orphans despite having CloudFormation tags
|
|
151
|
+
expect(orphans).toHaveLength(2);
|
|
152
|
+
|
|
153
|
+
// Both orphaned VPCs should be identified
|
|
154
|
+
const orphanIds = orphans.map((o) => o.physicalId).sort();
|
|
155
|
+
expect(orphanIds).toEqual(['vpc-0e2351eac99adcb83', 'vpc-0eadd96976d29ede7']);
|
|
156
|
+
|
|
157
|
+
// Each orphan should have proper reason
|
|
158
|
+
for (const orphan of orphans) {
|
|
159
|
+
expect(orphan.isOrphaned).toBe(true);
|
|
160
|
+
expect(orphan.reason).toContain(
|
|
161
|
+
'has CloudFormation tag for stack quo-integrations-dev but is not actually managed by the stack'
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('should NOT flag resources that are actually in the stack even with CloudFormation tags', async () => {
|
|
167
|
+
const stackIdentifier = new StackIdentifier({
|
|
168
|
+
stackName: 'my-app-prod',
|
|
169
|
+
region: 'us-east-1',
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Stack HAS a VPC in CloudFormation
|
|
173
|
+
const stackResources = [
|
|
174
|
+
{
|
|
175
|
+
logicalId: 'MyVPC',
|
|
176
|
+
physicalId: 'vpc-in-stack',
|
|
177
|
+
resourceType: 'AWS::EC2::VPC',
|
|
178
|
+
},
|
|
179
|
+
];
|
|
180
|
+
|
|
181
|
+
mockEC2Send.mockResolvedValue({
|
|
182
|
+
Vpcs: [
|
|
183
|
+
{
|
|
184
|
+
// VPC is in the stack AND has CloudFormation tag - NOT orphaned
|
|
185
|
+
VpcId: 'vpc-in-stack',
|
|
186
|
+
CidrBlock: '10.0.0.0/16',
|
|
187
|
+
State: 'available',
|
|
188
|
+
Tags: [
|
|
189
|
+
{ Key: 'aws:cloudformation:stack-name', Value: 'my-app-prod' },
|
|
190
|
+
],
|
|
191
|
+
},
|
|
192
|
+
],
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const orphans = await detector.findOrphanedResources({
|
|
196
|
+
stackIdentifier,
|
|
197
|
+
stackResources,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// Should find NO orphans - VPC is actually in the stack
|
|
201
|
+
expect(orphans).toEqual([]);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test('should detect mixed scenario: some resources in stack, some orphaned with same tags', async () => {
|
|
205
|
+
const stackIdentifier = new StackIdentifier({
|
|
206
|
+
stackName: 'test-stack',
|
|
207
|
+
region: 'us-east-1',
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// Stack has 2 subnets
|
|
211
|
+
const stackResources = [
|
|
212
|
+
{
|
|
213
|
+
logicalId: 'Subnet1',
|
|
214
|
+
physicalId: 'subnet-in-stack-1',
|
|
215
|
+
resourceType: 'AWS::EC2::Subnet',
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
logicalId: 'Subnet2',
|
|
219
|
+
physicalId: 'subnet-in-stack-2',
|
|
220
|
+
resourceType: 'AWS::EC2::Subnet',
|
|
221
|
+
},
|
|
222
|
+
];
|
|
223
|
+
|
|
224
|
+
mockEC2Send.mockResolvedValue({
|
|
225
|
+
Subnets: [
|
|
226
|
+
{
|
|
227
|
+
// Subnet #1: In stack - NOT orphaned
|
|
228
|
+
SubnetId: 'subnet-in-stack-1',
|
|
229
|
+
VpcId: 'vpc-123',
|
|
230
|
+
CidrBlock: '10.0.1.0/24',
|
|
231
|
+
AvailabilityZone: 'us-east-1a',
|
|
232
|
+
State: 'available',
|
|
233
|
+
Tags: [
|
|
234
|
+
{ Key: 'aws:cloudformation:stack-name', Value: 'test-stack' },
|
|
235
|
+
],
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
// Subnet #2: In stack - NOT orphaned
|
|
239
|
+
SubnetId: 'subnet-in-stack-2',
|
|
240
|
+
VpcId: 'vpc-123',
|
|
241
|
+
CidrBlock: '10.0.2.0/24',
|
|
242
|
+
AvailabilityZone: 'us-east-1b',
|
|
243
|
+
State: 'available',
|
|
244
|
+
Tags: [
|
|
245
|
+
{ Key: 'aws:cloudformation:stack-name', Value: 'test-stack' },
|
|
246
|
+
],
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
// Subnet #3: Has CloudFormation tag BUT not in stack - IS ORPHANED
|
|
250
|
+
SubnetId: 'subnet-orphan',
|
|
251
|
+
VpcId: 'vpc-123',
|
|
252
|
+
CidrBlock: '10.0.3.0/24',
|
|
253
|
+
AvailabilityZone: 'us-east-1c',
|
|
254
|
+
State: 'available',
|
|
255
|
+
Tags: [
|
|
256
|
+
{ Key: 'aws:cloudformation:stack-name', Value: 'test-stack' },
|
|
257
|
+
{ Key: 'Note', Value: 'Manually created and tagged' },
|
|
258
|
+
],
|
|
259
|
+
},
|
|
260
|
+
],
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const orphans = await detector.findOrphanedResources({
|
|
264
|
+
stackIdentifier,
|
|
265
|
+
stackResources,
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// Should find 1 orphan (subnet-orphan)
|
|
269
|
+
expect(orphans).toHaveLength(1);
|
|
270
|
+
expect(orphans[0].physicalId).toBe('subnet-orphan');
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test('should handle stack with no resources of checked types', async () => {
|
|
274
|
+
const stackIdentifier = new StackIdentifier({
|
|
275
|
+
stackName: 'lambda-only-stack',
|
|
276
|
+
region: 'us-east-1',
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// Stack only has Lambda functions (no VPCs, no subnets)
|
|
280
|
+
const stackResources = [
|
|
281
|
+
{
|
|
282
|
+
logicalId: 'MyFunction',
|
|
283
|
+
physicalId: 'my-function',
|
|
284
|
+
resourceType: 'AWS::Lambda::Function',
|
|
285
|
+
},
|
|
286
|
+
];
|
|
287
|
+
|
|
288
|
+
// Region has VPCs with CloudFormation tags for this stack
|
|
289
|
+
mockEC2Send.mockResolvedValue({
|
|
290
|
+
Vpcs: [
|
|
291
|
+
{
|
|
292
|
+
VpcId: 'vpc-orphan',
|
|
293
|
+
CidrBlock: '10.0.0.0/16',
|
|
294
|
+
State: 'available',
|
|
295
|
+
Tags: [
|
|
296
|
+
{ Key: 'aws:cloudformation:stack-name', Value: 'lambda-only-stack' },
|
|
297
|
+
],
|
|
298
|
+
},
|
|
299
|
+
],
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
const orphans = await detector.findOrphanedResources({
|
|
303
|
+
stackIdentifier,
|
|
304
|
+
stackResources,
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// Should find 1 orphan VPC (stack doesn't manage any VPCs)
|
|
308
|
+
expect(orphans).toHaveLength(1);
|
|
309
|
+
expect(orphans[0].physicalId).toBe('vpc-orphan');
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
});
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TDD Test for Orphan Detection with Multiple Stacks
|
|
3
|
+
*
|
|
4
|
+
* Bug: findOrphanedResources marks ALL resources in region as orphaned,
|
|
5
|
+
* including resources from other CloudFormation stacks and default AWS resources.
|
|
6
|
+
*
|
|
7
|
+
* Expected Behavior:
|
|
8
|
+
* - Only detect resources with frigg:stack tag matching target stack
|
|
9
|
+
* - Exclude resources managed by CloudFormation (aws:cloudformation:stack-name tag)
|
|
10
|
+
* - Exclude default AWS resources (default VPC, AWS-managed KMS keys)
|
|
11
|
+
* - Only check resource types that exist in stack template
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const AWSResourceDetector = require('../aws-resource-detector');
|
|
15
|
+
const StackIdentifier = require('../../../domain/value-objects/stack-identifier');
|
|
16
|
+
|
|
17
|
+
// Mock AWS SDK
|
|
18
|
+
jest.mock('@aws-sdk/client-ec2', () => ({
|
|
19
|
+
EC2Client: jest.fn(),
|
|
20
|
+
DescribeVpcsCommand: jest.fn(),
|
|
21
|
+
DescribeSubnetsCommand: jest.fn(),
|
|
22
|
+
DescribeSecurityGroupsCommand: jest.fn(),
|
|
23
|
+
DescribeRouteTablesCommand: jest.fn(),
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
jest.mock('@aws-sdk/client-rds', () => ({
|
|
27
|
+
RDSClient: jest.fn(),
|
|
28
|
+
DescribeDBClustersCommand: jest.fn(),
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
jest.mock('@aws-sdk/client-kms', () => ({
|
|
32
|
+
KMSClient: jest.fn(),
|
|
33
|
+
ListKeysCommand: jest.fn(),
|
|
34
|
+
DescribeKeyCommand: jest.fn(),
|
|
35
|
+
ListAliasesCommand: jest.fn(),
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
jest.mock('@aws-sdk/client-cloudformation', () => ({
|
|
39
|
+
CloudFormationClient: jest.fn(),
|
|
40
|
+
DescribeStackResourcesCommand: jest.fn(),
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
describe('Orphan Detection with Multiple Stacks (TDD)', () => {
|
|
44
|
+
let detector;
|
|
45
|
+
let mockEC2Send;
|
|
46
|
+
let mockRDSSend;
|
|
47
|
+
let mockKMSSend;
|
|
48
|
+
let mockCFSend;
|
|
49
|
+
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
jest.clearAllMocks();
|
|
52
|
+
|
|
53
|
+
// Mock EC2 client
|
|
54
|
+
mockEC2Send = jest.fn();
|
|
55
|
+
const { EC2Client } = require('@aws-sdk/client-ec2');
|
|
56
|
+
EC2Client.mockImplementation(() => ({ send: mockEC2Send }));
|
|
57
|
+
|
|
58
|
+
// Mock RDS client - return empty arrays by default
|
|
59
|
+
mockRDSSend = jest.fn().mockResolvedValue({ DBClusters: [] });
|
|
60
|
+
const { RDSClient } = require('@aws-sdk/client-rds');
|
|
61
|
+
RDSClient.mockImplementation(() => ({ send: mockRDSSend }));
|
|
62
|
+
|
|
63
|
+
// Mock KMS client - return empty arrays by default
|
|
64
|
+
mockKMSSend = jest.fn().mockResolvedValue({ Keys: [] });
|
|
65
|
+
const { KMSClient } = require('@aws-sdk/client-kms');
|
|
66
|
+
KMSClient.mockImplementation(() => ({ send: mockKMSSend }));
|
|
67
|
+
|
|
68
|
+
// Mock CloudFormation client
|
|
69
|
+
mockCFSend = jest.fn();
|
|
70
|
+
const { CloudFormationClient } = require('@aws-sdk/client-cloudformation');
|
|
71
|
+
CloudFormationClient.mockImplementation(() => ({ send: mockCFSend }));
|
|
72
|
+
|
|
73
|
+
detector = new AWSResourceDetector({ region: 'us-east-1' });
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('Scenario: Multiple stacks and default resources in same region', () => {
|
|
77
|
+
test('should only detect orphans with frigg:stack tag matching target stack', async () => {
|
|
78
|
+
const stackIdentifier = new StackIdentifier({
|
|
79
|
+
stackName: 'quo-integrations-dev',
|
|
80
|
+
region: 'us-east-1',
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Stack resources from CloudFormation (what's in the template)
|
|
84
|
+
const stackResources = [
|
|
85
|
+
{
|
|
86
|
+
logicalId: 'MyVPC',
|
|
87
|
+
physicalId: 'vpc-stack-managed',
|
|
88
|
+
resourceType: 'AWS::EC2::VPC',
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
logicalId: 'MyDBCluster',
|
|
92
|
+
physicalId: 'dev-cluster',
|
|
93
|
+
resourceType: 'AWS::RDS::DBCluster',
|
|
94
|
+
},
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
// Mock CloudFormation stack resources
|
|
98
|
+
mockCFSend.mockResolvedValue({
|
|
99
|
+
StackResources: [
|
|
100
|
+
{
|
|
101
|
+
LogicalResourceId: 'MyVPC',
|
|
102
|
+
PhysicalResourceId: 'vpc-stack-managed',
|
|
103
|
+
ResourceType: 'AWS::EC2::VPC',
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
LogicalResourceId: 'MyDBCluster',
|
|
107
|
+
PhysicalResourceId: 'dev-cluster',
|
|
108
|
+
ResourceType: 'AWS::RDS::DBCluster',
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Mock EC2 DescribeVpcs - returns 5 VPCs in region
|
|
114
|
+
mockEC2Send.mockResolvedValue({
|
|
115
|
+
Vpcs: [
|
|
116
|
+
{
|
|
117
|
+
// VPC #1: Managed by CloudFormation for this stack - NOT orphaned
|
|
118
|
+
VpcId: 'vpc-stack-managed',
|
|
119
|
+
CidrBlock: '10.0.0.0/16',
|
|
120
|
+
State: 'available',
|
|
121
|
+
Tags: [
|
|
122
|
+
{ Key: 'aws:cloudformation:stack-name', Value: 'quo-integrations-dev' },
|
|
123
|
+
{ Key: 'frigg:stack', Value: 'quo-integrations-dev' },
|
|
124
|
+
],
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
// VPC #2: Has frigg:stack tag but not in CloudFormation - IS ORPHANED
|
|
128
|
+
VpcId: 'vpc-orphan',
|
|
129
|
+
CidrBlock: '10.1.0.0/16',
|
|
130
|
+
State: 'available',
|
|
131
|
+
Tags: [
|
|
132
|
+
{ Key: 'frigg:stack', Value: 'quo-integrations-dev' },
|
|
133
|
+
// No aws:cloudformation:stack-name tag = orphan
|
|
134
|
+
],
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
// VPC #3: Managed by DIFFERENT CloudFormation stack - NOT orphaned
|
|
138
|
+
VpcId: 'vpc-other-stack',
|
|
139
|
+
CidrBlock: '10.2.0.0/16',
|
|
140
|
+
State: 'available',
|
|
141
|
+
Tags: [
|
|
142
|
+
{ Key: 'aws:cloudformation:stack-name', Value: 'quo-integrations-prod' },
|
|
143
|
+
{ Key: 'frigg:stack', Value: 'quo-integrations-prod' },
|
|
144
|
+
],
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
// VPC #4: Default VPC (no tags) - NOT orphaned
|
|
148
|
+
VpcId: 'vpc-default',
|
|
149
|
+
CidrBlock: '172.31.0.0/16',
|
|
150
|
+
State: 'available',
|
|
151
|
+
IsDefault: true,
|
|
152
|
+
Tags: [],
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
// VPC #5: Random VPC with no frigg tags - NOT orphaned
|
|
156
|
+
VpcId: 'vpc-unrelated',
|
|
157
|
+
CidrBlock: '192.168.0.0/16',
|
|
158
|
+
State: 'available',
|
|
159
|
+
Tags: [{ Key: 'Team', Value: 'platform' }],
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Mock RDS DescribeDBClusters - returns 2 clusters in region
|
|
165
|
+
mockRDSSend.mockResolvedValue({
|
|
166
|
+
DBClusters: [
|
|
167
|
+
{
|
|
168
|
+
// Cluster #1: Managed by CloudFormation for this stack - NOT orphaned
|
|
169
|
+
DBClusterIdentifier: 'dev-cluster',
|
|
170
|
+
Engine: 'aurora-postgresql',
|
|
171
|
+
Status: 'available',
|
|
172
|
+
ClusterCreateTime: new Date('2024-01-01'),
|
|
173
|
+
TagList: [
|
|
174
|
+
{ Key: 'aws:cloudformation:stack-name', Value: 'quo-integrations-dev' },
|
|
175
|
+
{ Key: 'frigg:stack', Value: 'quo-integrations-dev' },
|
|
176
|
+
],
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
// Cluster #2: From different stack - NOT orphaned (wrong frigg:stack tag)
|
|
180
|
+
DBClusterIdentifier: 'prod-cluster',
|
|
181
|
+
Engine: 'aurora-postgresql',
|
|
182
|
+
Status: 'available',
|
|
183
|
+
ClusterCreateTime: new Date('2024-01-01'),
|
|
184
|
+
TagList: [
|
|
185
|
+
{ Key: 'aws:cloudformation:stack-name', Value: 'quo-integrations-prod' },
|
|
186
|
+
{ Key: 'frigg:stack', Value: 'quo-integrations-prod' },
|
|
187
|
+
],
|
|
188
|
+
},
|
|
189
|
+
],
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// Act
|
|
193
|
+
const orphans = await detector.findOrphanedResources({
|
|
194
|
+
stackIdentifier,
|
|
195
|
+
stackResources,
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Assert
|
|
199
|
+
// Should ONLY find vpc-orphan (has frigg:stack=quo-integrations-dev but no CloudFormation tag)
|
|
200
|
+
expect(orphans).toHaveLength(1);
|
|
201
|
+
expect(orphans[0].physicalId).toBe('vpc-orphan');
|
|
202
|
+
expect(orphans[0].resourceType).toBe('AWS::EC2::VPC');
|
|
203
|
+
|
|
204
|
+
// Should NOT include:
|
|
205
|
+
// - vpc-stack-managed (managed by CloudFormation)
|
|
206
|
+
// - vpc-other-stack (different stack)
|
|
207
|
+
// - vpc-default (no frigg tag)
|
|
208
|
+
// - vpc-unrelated (no frigg tag)
|
|
209
|
+
// - dev-cluster (managed by CloudFormation)
|
|
210
|
+
// - prod-cluster (different stack)
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test('should handle case where stack has no orphans', async () => {
|
|
214
|
+
const stackIdentifier = new StackIdentifier({
|
|
215
|
+
stackName: 'perfect-stack',
|
|
216
|
+
region: 'us-east-1',
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const stackResources = [
|
|
220
|
+
{
|
|
221
|
+
logicalId: 'MyVPC',
|
|
222
|
+
physicalId: 'vpc-perfect',
|
|
223
|
+
resourceType: 'AWS::EC2::VPC',
|
|
224
|
+
},
|
|
225
|
+
];
|
|
226
|
+
|
|
227
|
+
mockCFSend.mockResolvedValue({
|
|
228
|
+
StackResources: [
|
|
229
|
+
{
|
|
230
|
+
LogicalResourceId: 'MyVPC',
|
|
231
|
+
PhysicalResourceId: 'vpc-perfect',
|
|
232
|
+
ResourceType: 'AWS::EC2::VPC',
|
|
233
|
+
},
|
|
234
|
+
],
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// All VPCs are either CloudFormation-managed or belong to other stacks
|
|
238
|
+
mockEC2Send.mockResolvedValue({
|
|
239
|
+
Vpcs: [
|
|
240
|
+
{
|
|
241
|
+
VpcId: 'vpc-perfect',
|
|
242
|
+
CidrBlock: '10.0.0.0/16',
|
|
243
|
+
State: 'available',
|
|
244
|
+
Tags: [
|
|
245
|
+
{ Key: 'aws:cloudformation:stack-name', Value: 'perfect-stack' },
|
|
246
|
+
{ Key: 'frigg:stack', Value: 'perfect-stack' },
|
|
247
|
+
],
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
VpcId: 'vpc-other',
|
|
251
|
+
CidrBlock: '10.1.0.0/16',
|
|
252
|
+
State: 'available',
|
|
253
|
+
Tags: [{ Key: 'frigg:stack', Value: 'other-stack' }],
|
|
254
|
+
},
|
|
255
|
+
],
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
const orphans = await detector.findOrphanedResources({
|
|
259
|
+
stackIdentifier,
|
|
260
|
+
stackResources,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// Should find NO orphans
|
|
264
|
+
expect(orphans).toEqual([]);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test('should check all supported resource types for orphans (not just types in stack)', async () => {
|
|
268
|
+
const stackIdentifier = new StackIdentifier({
|
|
269
|
+
stackName: 'simple-stack',
|
|
270
|
+
region: 'us-east-1',
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// Stack only has VPC resources - no RDS, no KMS
|
|
274
|
+
const stackResources = [
|
|
275
|
+
{
|
|
276
|
+
logicalId: 'MyVPC',
|
|
277
|
+
physicalId: 'vpc-simple',
|
|
278
|
+
resourceType: 'AWS::EC2::VPC',
|
|
279
|
+
},
|
|
280
|
+
];
|
|
281
|
+
|
|
282
|
+
mockCFSend.mockResolvedValue({
|
|
283
|
+
StackResources: [
|
|
284
|
+
{
|
|
285
|
+
LogicalResourceId: 'MyVPC',
|
|
286
|
+
PhysicalResourceId: 'vpc-simple',
|
|
287
|
+
ResourceType: 'AWS::EC2::VPC',
|
|
288
|
+
},
|
|
289
|
+
],
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
mockEC2Send.mockResolvedValue({
|
|
293
|
+
Vpcs: [
|
|
294
|
+
{
|
|
295
|
+
VpcId: 'vpc-simple',
|
|
296
|
+
CidrBlock: '10.0.0.0/16',
|
|
297
|
+
State: 'available',
|
|
298
|
+
Tags: [{ Key: 'aws:cloudformation:stack-name', Value: 'simple-stack' }],
|
|
299
|
+
},
|
|
300
|
+
],
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
await detector.findOrphanedResources({
|
|
304
|
+
stackIdentifier,
|
|
305
|
+
stackResources,
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// Should check ALL resource types (EC2, RDS, KMS) even if stack only has VPC
|
|
309
|
+
// This is because orphaned resources by definition are NOT in the stack
|
|
310
|
+
expect(mockEC2Send).toHaveBeenCalled();
|
|
311
|
+
expect(mockRDSSend).toHaveBeenCalled(); // Changed: now checks all types
|
|
312
|
+
expect(mockKMSSend).toHaveBeenCalled(); // Changed: now checks all types
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test('should filter out default VPCs', async () => {
|
|
316
|
+
const stackIdentifier = new StackIdentifier({
|
|
317
|
+
stackName: 'my-stack',
|
|
318
|
+
region: 'us-east-1',
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
const stackResources = [
|
|
322
|
+
{
|
|
323
|
+
logicalId: 'MyVPC',
|
|
324
|
+
physicalId: 'vpc-custom',
|
|
325
|
+
resourceType: 'AWS::EC2::VPC',
|
|
326
|
+
},
|
|
327
|
+
];
|
|
328
|
+
|
|
329
|
+
mockCFSend.mockResolvedValue({
|
|
330
|
+
StackResources: [
|
|
331
|
+
{
|
|
332
|
+
LogicalResourceId: 'MyVPC',
|
|
333
|
+
PhysicalResourceId: 'vpc-custom',
|
|
334
|
+
ResourceType: 'AWS::EC2::VPC',
|
|
335
|
+
},
|
|
336
|
+
],
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
mockEC2Send.mockResolvedValue({
|
|
340
|
+
Vpcs: [
|
|
341
|
+
{
|
|
342
|
+
VpcId: 'vpc-custom',
|
|
343
|
+
CidrBlock: '10.0.0.0/16',
|
|
344
|
+
State: 'available',
|
|
345
|
+
Tags: [{ Key: 'aws:cloudformation:stack-name', Value: 'my-stack' }],
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
// Default VPC - should be filtered out even with frigg tag
|
|
349
|
+
VpcId: 'vpc-default',
|
|
350
|
+
CidrBlock: '172.31.0.0/16',
|
|
351
|
+
State: 'available',
|
|
352
|
+
IsDefault: true,
|
|
353
|
+
Tags: [{ Key: 'frigg:stack', Value: 'my-stack' }],
|
|
354
|
+
},
|
|
355
|
+
],
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
const orphans = await detector.findOrphanedResources({
|
|
359
|
+
stackIdentifier,
|
|
360
|
+
stackResources,
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
// Should NOT include default VPC
|
|
364
|
+
expect(orphans).toEqual([]);
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
});
|