@friggframework/devtools 2.0.0--canary.474.86c5119.0 → 2.0.0--canary.474.6a0bba7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/infrastructure/domains/database/migration-builder.js +199 -1
- package/infrastructure/domains/database/migration-builder.test.js +73 -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/reconcile-properties-use-case.js +6 -0
- package/infrastructure/domains/health/application/use-cases/repair-via-import-use-case.js +307 -1
- package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.js +38 -5
- package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.test.js +3 -3
- 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 +50 -1
- package/infrastructure/domains/health/domain/entities/issue.test.js +111 -0
- package/infrastructure/domains/health/domain/services/__tests__/health-score-percentage-based.test.js +380 -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/health-score-calculator.js +174 -91
- package/infrastructure/domains/health/domain/services/health-score-calculator.test.js +332 -228
- package/infrastructure/domains/health/domain/services/logical-id-mapper.js +345 -0
- package/infrastructure/domains/health/domain/services/template-parser.js +245 -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 +407 -20
- package/infrastructure/domains/health/infrastructure/adapters/aws-property-reconciler.test.js +698 -26
- package/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector.js +108 -14
- package/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector.test.js +69 -12
- package/infrastructure/domains/health/infrastructure/adapters/aws-stack-repository.js +392 -1
- package/package.json +6 -6
|
@@ -414,4 +414,115 @@ describe('Issue', () => {
|
|
|
414
414
|
expect(issue.canAutoFix).toBe(false); // Can't auto-fix immutable
|
|
415
415
|
});
|
|
416
416
|
});
|
|
417
|
+
|
|
418
|
+
describe('_formatValue', () => {
|
|
419
|
+
it('should format primitive values', () => {
|
|
420
|
+
expect(Issue._formatValue('test')).toBe('test');
|
|
421
|
+
expect(Issue._formatValue(123)).toBe('123');
|
|
422
|
+
expect(Issue._formatValue(true)).toBe('true');
|
|
423
|
+
expect(Issue._formatValue(null)).toBe('null');
|
|
424
|
+
expect(Issue._formatValue(undefined)).toBe('undefined');
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it('should format simple arrays', () => {
|
|
428
|
+
expect(Issue._formatValue(['a', 'b', 'c'])).toBe('["a","b","c"]');
|
|
429
|
+
expect(Issue._formatValue([1, 2, 3])).toBe('[1,2,3]');
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it('should format arrays of objects (like Tags)', () => {
|
|
433
|
+
const tags = [
|
|
434
|
+
{ Key: 'Name', Value: 'test' },
|
|
435
|
+
{ Key: 'Environment', Value: 'prod' },
|
|
436
|
+
];
|
|
437
|
+
const result = Issue._formatValue(tags);
|
|
438
|
+
expect(result).toContain('Key');
|
|
439
|
+
expect(result).toContain('Name');
|
|
440
|
+
expect(result).toContain('test');
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it('should truncate long arrays of objects', () => {
|
|
444
|
+
const manyTags = [
|
|
445
|
+
{ Key: 'Tag1', Value: 'val1' },
|
|
446
|
+
{ Key: 'Tag2', Value: 'val2' },
|
|
447
|
+
{ Key: 'Tag3', Value: 'val3' },
|
|
448
|
+
{ Key: 'Tag4', Value: 'val4' },
|
|
449
|
+
];
|
|
450
|
+
const result = Issue._formatValue(manyTags);
|
|
451
|
+
expect(result).toContain('4 total');
|
|
452
|
+
expect(result).toContain('...');
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it('should format small objects', () => {
|
|
456
|
+
const obj = { a: 1, b: 2 };
|
|
457
|
+
expect(Issue._formatValue(obj)).toBe('{"a":1,"b":2}');
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it('should truncate large objects', () => {
|
|
461
|
+
const largeObj = { a: 1, b: 2, c: 3, d: 4, e: 5 };
|
|
462
|
+
const result = Issue._formatValue(largeObj);
|
|
463
|
+
expect(result).toContain('5 keys total');
|
|
464
|
+
expect(result).toContain('...');
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it('should format empty collections', () => {
|
|
468
|
+
expect(Issue._formatValue([])).toBe('[]');
|
|
469
|
+
expect(Issue._formatValue({})).toBe('{}');
|
|
470
|
+
});
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
describe('propertyMismatch with formatted values', () => {
|
|
474
|
+
it('should format Tags arrays correctly in description', () => {
|
|
475
|
+
const mismatch = new PropertyMismatch({
|
|
476
|
+
propertyPath: 'Properties.Tags',
|
|
477
|
+
expectedValue: [
|
|
478
|
+
{ Key: 'Name', Value: 'test' },
|
|
479
|
+
{ Key: 'Environment', Value: 'prod' },
|
|
480
|
+
],
|
|
481
|
+
actualValue: [
|
|
482
|
+
{ Key: 'Name', Value: 'test' },
|
|
483
|
+
{ Key: 'Environment', Value: 'prod' },
|
|
484
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
485
|
+
],
|
|
486
|
+
mutability: PropertyMutability.MUTABLE,
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
const issue = Issue.propertyMismatch({
|
|
490
|
+
resourceType: 'AWS::EC2::Subnet',
|
|
491
|
+
resourceId: 'subnet-123',
|
|
492
|
+
mismatch,
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
// Should not contain [object Object]
|
|
496
|
+
expect(issue.description).not.toContain('[object Object]');
|
|
497
|
+
// Should contain JSON representation
|
|
498
|
+
expect(issue.description).toContain('Key');
|
|
499
|
+
expect(issue.description).toContain('Name');
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
it('should handle complex nested objects', () => {
|
|
503
|
+
const mismatch = new PropertyMismatch({
|
|
504
|
+
propertyPath: 'Properties.VpcConfig',
|
|
505
|
+
expectedValue: {
|
|
506
|
+
SubnetIds: ['subnet-1', 'subnet-2'],
|
|
507
|
+
SecurityGroupIds: ['sg-1'],
|
|
508
|
+
},
|
|
509
|
+
actualValue: {
|
|
510
|
+
SubnetIds: ['subnet-3', 'subnet-4'],
|
|
511
|
+
SecurityGroupIds: ['sg-2'],
|
|
512
|
+
},
|
|
513
|
+
mutability: PropertyMutability.MUTABLE,
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
const issue = Issue.propertyMismatch({
|
|
517
|
+
resourceType: 'AWS::Lambda::Function',
|
|
518
|
+
resourceId: 'my-function',
|
|
519
|
+
mismatch,
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
// Should not contain [object Object]
|
|
523
|
+
expect(issue.description).not.toContain('[object Object]');
|
|
524
|
+
// Should contain structured representation
|
|
525
|
+
expect(issue.description).toContain('SubnetIds');
|
|
526
|
+
});
|
|
527
|
+
});
|
|
417
528
|
});
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TDD Test for Percentage-Based Health Score Calculation
|
|
3
|
+
*
|
|
4
|
+
* PROBLEM: Current health score uses fixed penalties per issue:
|
|
5
|
+
* - Critical: 30 points
|
|
6
|
+
* - Warning: 10 points
|
|
7
|
+
* - Info: 5 points
|
|
8
|
+
*
|
|
9
|
+
* This causes misleading scores. Example from quo-integrations-dev:
|
|
10
|
+
* - 111 total resources
|
|
11
|
+
* - 16 Lambda functions with VPC config drift (32 warnings × 10 = 320 points)
|
|
12
|
+
* - Score: 0/100 (meaningless!)
|
|
13
|
+
*
|
|
14
|
+
* The 16 Lambdas with VPC drift are concerning but not catastrophic.
|
|
15
|
+
* VPC config drift on ALL Lambdas should score ~70/100, not 0/100.
|
|
16
|
+
*
|
|
17
|
+
* SOLUTION: Percentage-based scoring with resource criticality weighting:
|
|
18
|
+
*
|
|
19
|
+
* 1. Categorize resources by criticality:
|
|
20
|
+
* - Critical: Lambda, RDS, DynamoDB (affect application functionality)
|
|
21
|
+
* - Infrastructure: VPC, Subnet, SecurityGroup, KMS, etc.
|
|
22
|
+
*
|
|
23
|
+
* 2. Calculate impact percentages:
|
|
24
|
+
* - Critical impact % = critical issues / total resources
|
|
25
|
+
* - Functional drift % = functional drift / critical resources
|
|
26
|
+
* - Infra drift % = infra drift / infrastructure resources
|
|
27
|
+
*
|
|
28
|
+
* 3. Weighted penalties (max 100 points):
|
|
29
|
+
* - Critical issues: up to 50 points (orphaned, missing resources)
|
|
30
|
+
* - Functional drift: up to 30 points (drift on Lambda/RDS/DynamoDB)
|
|
31
|
+
* - Infrastructure drift: up to 20 points (drift on VPC/networking/KMS)
|
|
32
|
+
*
|
|
33
|
+
* EXAMPLES:
|
|
34
|
+
*
|
|
35
|
+
* Example 1: quo-integrations-dev
|
|
36
|
+
* - 111 resources (16 Lambda, 95 infrastructure)
|
|
37
|
+
* - 0 critical issues
|
|
38
|
+
* - 16 Lambdas with VPC drift = 100% functional drift
|
|
39
|
+
* - Penalty: (100% × 30) = 30 points
|
|
40
|
+
* - Score: 70/100 ✅ (was 0/100)
|
|
41
|
+
*
|
|
42
|
+
* Example 2: Stack with orphaned resources
|
|
43
|
+
* - 50 resources
|
|
44
|
+
* - 2 orphaned VPCs = 4% critical impact
|
|
45
|
+
* - Penalty: (4% × 50) = 2 points
|
|
46
|
+
* - Score: 98/100 ✅
|
|
47
|
+
*
|
|
48
|
+
* Example 3: Stack with missing Lambda
|
|
49
|
+
* - 10 resources (5 Lambda, 5 infrastructure)
|
|
50
|
+
* - 1 missing Lambda = 10% critical impact
|
|
51
|
+
* - Penalty: (10% × 50) = 5 points
|
|
52
|
+
* - Score: 95/100 ✅
|
|
53
|
+
*
|
|
54
|
+
* Example 4: Complete disaster
|
|
55
|
+
* - 20 resources (10 Lambda, 10 infrastructure)
|
|
56
|
+
* - 5 missing Lambdas = 25% critical impact
|
|
57
|
+
* - 5 Lambdas drifted = 50% functional drift
|
|
58
|
+
* - 10 infrastructure drifted = 100% infra drift
|
|
59
|
+
* - Penalty: (25% × 50) + (50% × 30) + (100% × 20) = 12.5 + 15 + 20 = 47.5
|
|
60
|
+
* - Score: 52.5/100 ✅ (reflects severity)
|
|
61
|
+
*/
|
|
62
|
+
|
|
63
|
+
const HealthScoreCalculator = require('../health-score-calculator');
|
|
64
|
+
const HealthScore = require('../../value-objects/health-score');
|
|
65
|
+
const Issue = require('../../entities/issue');
|
|
66
|
+
const Resource = require('../../entities/resource');
|
|
67
|
+
const ResourceState = require('../../value-objects/resource-state');
|
|
68
|
+
const PropertyMismatch = require('../../entities/property-mismatch');
|
|
69
|
+
const PropertyMutability = require('../../value-objects/property-mutability');
|
|
70
|
+
|
|
71
|
+
describe('Percentage-Based Health Score Calculation (TDD)', () => {
|
|
72
|
+
let calculator;
|
|
73
|
+
|
|
74
|
+
beforeEach(() => {
|
|
75
|
+
calculator = new HealthScoreCalculator();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('Real-world scenario: quo-integrations-dev VPC drift', () => {
|
|
79
|
+
test('should score 70/100 for VPC config drift on all Lambdas (not 0/100)', () => {
|
|
80
|
+
// 111 resources: 16 Lambda functions, 95 infrastructure
|
|
81
|
+
const resources = [
|
|
82
|
+
// 16 Lambda functions with VPC drift
|
|
83
|
+
...Array.from({ length: 16 }, (_, i) => ({
|
|
84
|
+
logicalId: `Lambda${i}`,
|
|
85
|
+
physicalId: `lambda-${i}`,
|
|
86
|
+
resourceType: 'AWS::Lambda::Function',
|
|
87
|
+
state: ResourceState.DRIFTED,
|
|
88
|
+
})),
|
|
89
|
+
// 95 infrastructure resources (in sync)
|
|
90
|
+
...Array.from({ length: 95 }, (_, i) => ({
|
|
91
|
+
logicalId: `Infra${i}`,
|
|
92
|
+
physicalId: `infra-${i}`,
|
|
93
|
+
resourceType: 'AWS::EC2::SecurityGroup',
|
|
94
|
+
state: ResourceState.IN_STACK,
|
|
95
|
+
})),
|
|
96
|
+
].map((r) => new Resource(r));
|
|
97
|
+
|
|
98
|
+
// 32 warnings: 2 per Lambda (SecurityGroupIds + SubnetIds drift)
|
|
99
|
+
const issues = Array.from({ length: 32 }, (_, i) => {
|
|
100
|
+
const lambdaIndex = Math.floor(i / 2);
|
|
101
|
+
const property = i % 2 === 0 ? 'VpcConfig.SecurityGroupIds' : 'VpcConfig.SubnetIds';
|
|
102
|
+
|
|
103
|
+
return Issue.propertyMismatch({
|
|
104
|
+
resourceType: 'AWS::Lambda::Function',
|
|
105
|
+
resourceId: `lambda-${lambdaIndex}`,
|
|
106
|
+
mismatch: new PropertyMismatch({
|
|
107
|
+
propertyPath: property,
|
|
108
|
+
expectedValue: 'expected',
|
|
109
|
+
actualValue: 'actual',
|
|
110
|
+
mutability: PropertyMutability.MUTABLE,
|
|
111
|
+
}),
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Act
|
|
116
|
+
const score = calculator.calculate({ resources, issues });
|
|
117
|
+
|
|
118
|
+
// Assert
|
|
119
|
+
// Functional drift: 16/16 = 100% of critical resources drifted
|
|
120
|
+
// Penalty: 100% × 30 = 30 points
|
|
121
|
+
// Score: 100 - 30 = 70
|
|
122
|
+
expect(score.value).toBe(70);
|
|
123
|
+
expect(score.isHealthy()).toBe(false); // Below 80 threshold
|
|
124
|
+
expect(score.isDegraded()).toBe(true); // 40-79 is degraded
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe('Critical issues (orphaned, missing resources)', () => {
|
|
129
|
+
test('should apply minimal penalty for small percentage of orphaned resources', () => {
|
|
130
|
+
// 50 resources, 2 orphaned VPCs
|
|
131
|
+
const resources = [
|
|
132
|
+
...Array.from({ length: 48 }, (_, i) => ({
|
|
133
|
+
logicalId: `Resource${i}`,
|
|
134
|
+
physicalId: `resource-${i}`,
|
|
135
|
+
resourceType: 'AWS::Lambda::Function',
|
|
136
|
+
state: ResourceState.IN_STACK,
|
|
137
|
+
})),
|
|
138
|
+
new Resource({
|
|
139
|
+
logicalId: null,
|
|
140
|
+
physicalId: 'vpc-orphan-1',
|
|
141
|
+
resourceType: 'AWS::EC2::VPC',
|
|
142
|
+
state: ResourceState.ORPHANED,
|
|
143
|
+
}),
|
|
144
|
+
new Resource({
|
|
145
|
+
logicalId: null,
|
|
146
|
+
physicalId: 'vpc-orphan-2',
|
|
147
|
+
resourceType: 'AWS::EC2::VPC',
|
|
148
|
+
state: ResourceState.ORPHANED,
|
|
149
|
+
}),
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
const issues = [
|
|
153
|
+
Issue.orphanedResource({
|
|
154
|
+
resourceType: 'AWS::EC2::VPC',
|
|
155
|
+
resourceId: 'vpc-orphan-1',
|
|
156
|
+
description: 'Orphaned VPC',
|
|
157
|
+
}),
|
|
158
|
+
Issue.orphanedResource({
|
|
159
|
+
resourceType: 'AWS::EC2::VPC',
|
|
160
|
+
resourceId: 'vpc-orphan-2',
|
|
161
|
+
description: 'Orphaned VPC',
|
|
162
|
+
}),
|
|
163
|
+
];
|
|
164
|
+
|
|
165
|
+
const score = calculator.calculate({ resources, issues });
|
|
166
|
+
|
|
167
|
+
// Critical impact: 2/50 = 4%
|
|
168
|
+
// Penalty: 4% × 50 = 2 points
|
|
169
|
+
// Score: 100 - 2 = 98
|
|
170
|
+
expect(score.value).toBe(98);
|
|
171
|
+
expect(score.isHealthy()).toBe(true);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('should apply significant penalty for high percentage of missing critical resources', () => {
|
|
175
|
+
// 10 resources: 5 Lambda, 5 infrastructure
|
|
176
|
+
// 2 Lambdas missing
|
|
177
|
+
const resources = [
|
|
178
|
+
...Array.from({ length: 3 }, (_, i) => ({
|
|
179
|
+
logicalId: `Lambda${i}`,
|
|
180
|
+
physicalId: `lambda-${i}`,
|
|
181
|
+
resourceType: 'AWS::Lambda::Function',
|
|
182
|
+
state: ResourceState.IN_STACK,
|
|
183
|
+
})),
|
|
184
|
+
...Array.from({ length: 2 }, (_, i) => ({
|
|
185
|
+
logicalId: `MissingLambda${i}`,
|
|
186
|
+
physicalId: `missing-MissingLambda${i}`,
|
|
187
|
+
resourceType: 'AWS::Lambda::Function',
|
|
188
|
+
state: ResourceState.MISSING,
|
|
189
|
+
})),
|
|
190
|
+
...Array.from({ length: 5 }, (_, i) => ({
|
|
191
|
+
logicalId: `Infra${i}`,
|
|
192
|
+
physicalId: `infra-${i}`,
|
|
193
|
+
resourceType: 'AWS::EC2::VPC',
|
|
194
|
+
state: ResourceState.IN_STACK,
|
|
195
|
+
})),
|
|
196
|
+
].map((r) => new Resource(r));
|
|
197
|
+
|
|
198
|
+
const issues = [
|
|
199
|
+
Issue.missingResource({
|
|
200
|
+
resourceType: 'AWS::Lambda::Function',
|
|
201
|
+
resourceId: 'MissingLambda0',
|
|
202
|
+
description: 'Lambda function missing',
|
|
203
|
+
}),
|
|
204
|
+
Issue.missingResource({
|
|
205
|
+
resourceType: 'AWS::Lambda::Function',
|
|
206
|
+
resourceId: 'MissingLambda1',
|
|
207
|
+
description: 'Lambda function missing',
|
|
208
|
+
}),
|
|
209
|
+
];
|
|
210
|
+
|
|
211
|
+
const score = calculator.calculate({ resources, issues });
|
|
212
|
+
|
|
213
|
+
// Critical impact: 2/10 = 20%
|
|
214
|
+
// Penalty: 20% × 50 = 10 points
|
|
215
|
+
// Score: 100 - 10 = 90
|
|
216
|
+
expect(score.value).toBe(90);
|
|
217
|
+
expect(score.isHealthy()).toBe(true);
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
describe('Functional drift (Lambda, RDS, DynamoDB)', () => {
|
|
222
|
+
test('should weight functional drift higher than infrastructure drift', () => {
|
|
223
|
+
// 20 resources: 10 Lambda, 10 VPC
|
|
224
|
+
// 5 Lambdas drifted, 10 VPCs drifted
|
|
225
|
+
const resources = [
|
|
226
|
+
...Array.from({ length: 5 }, (_, i) => ({
|
|
227
|
+
logicalId: `DriftedLambda${i}`,
|
|
228
|
+
physicalId: `lambda-${i}`,
|
|
229
|
+
resourceType: 'AWS::Lambda::Function',
|
|
230
|
+
state: ResourceState.DRIFTED,
|
|
231
|
+
})),
|
|
232
|
+
...Array.from({ length: 5 }, (_, i) => ({
|
|
233
|
+
logicalId: `GoodLambda${i}`,
|
|
234
|
+
physicalId: `lambda-good-${i}`,
|
|
235
|
+
resourceType: 'AWS::Lambda::Function',
|
|
236
|
+
state: ResourceState.IN_STACK,
|
|
237
|
+
})),
|
|
238
|
+
...Array.from({ length: 10 }, (_, i) => ({
|
|
239
|
+
logicalId: `DriftedVPC${i}`,
|
|
240
|
+
physicalId: `vpc-${i}`,
|
|
241
|
+
resourceType: 'AWS::EC2::VPC',
|
|
242
|
+
state: ResourceState.DRIFTED,
|
|
243
|
+
})),
|
|
244
|
+
].map((r) => new Resource(r));
|
|
245
|
+
|
|
246
|
+
const issues = [
|
|
247
|
+
...Array.from({ length: 5 }, (_, i) =>
|
|
248
|
+
Issue.propertyMismatch({
|
|
249
|
+
resourceType: 'AWS::Lambda::Function',
|
|
250
|
+
resourceId: `lambda-${i}`,
|
|
251
|
+
mismatch: new PropertyMismatch({
|
|
252
|
+
propertyPath: 'Environment',
|
|
253
|
+
expectedValue: 'expected',
|
|
254
|
+
actualValue: 'actual',
|
|
255
|
+
mutability: PropertyMutability.MUTABLE,
|
|
256
|
+
}),
|
|
257
|
+
})
|
|
258
|
+
),
|
|
259
|
+
...Array.from({ length: 10 }, (_, i) =>
|
|
260
|
+
Issue.propertyMismatch({
|
|
261
|
+
resourceType: 'AWS::EC2::VPC',
|
|
262
|
+
resourceId: `vpc-${i}`,
|
|
263
|
+
mismatch: new PropertyMismatch({
|
|
264
|
+
propertyPath: 'Tags',
|
|
265
|
+
expectedValue: 'expected',
|
|
266
|
+
actualValue: 'actual',
|
|
267
|
+
mutability: PropertyMutability.MUTABLE,
|
|
268
|
+
}),
|
|
269
|
+
})
|
|
270
|
+
),
|
|
271
|
+
];
|
|
272
|
+
|
|
273
|
+
const score = calculator.calculate({ resources, issues });
|
|
274
|
+
|
|
275
|
+
// Functional drift: 5/10 = 50% → penalty: 50% × 30 = 15 points
|
|
276
|
+
// Infra drift: 10/10 = 100% → penalty: 100% × 20 = 20 points
|
|
277
|
+
// Total penalty: 15 + 20 = 35 points
|
|
278
|
+
// Score: 100 - 35 = 65
|
|
279
|
+
expect(score.value).toBe(65);
|
|
280
|
+
expect(score.isHealthy()).toBe(false); // Below 80 threshold
|
|
281
|
+
expect(score.isDegraded()).toBe(true); // 40-79 is degraded
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
describe('Complete disaster scenario', () => {
|
|
286
|
+
test('should reflect severity when everything is broken', () => {
|
|
287
|
+
// 20 resources: 10 Lambda, 10 infrastructure
|
|
288
|
+
// 5 Lambdas missing, 5 Lambdas drifted, 10 infrastructure drifted
|
|
289
|
+
const resources = [
|
|
290
|
+
...Array.from({ length: 5 }, (_, i) => ({
|
|
291
|
+
logicalId: `MissingLambda${i}`,
|
|
292
|
+
physicalId: `missing-MissingLambda${i}`,
|
|
293
|
+
resourceType: 'AWS::Lambda::Function',
|
|
294
|
+
state: ResourceState.MISSING,
|
|
295
|
+
})),
|
|
296
|
+
...Array.from({ length: 5 }, (_, i) => ({
|
|
297
|
+
logicalId: `DriftedLambda${i}`,
|
|
298
|
+
physicalId: `lambda-${i}`,
|
|
299
|
+
resourceType: 'AWS::Lambda::Function',
|
|
300
|
+
state: ResourceState.DRIFTED,
|
|
301
|
+
})),
|
|
302
|
+
...Array.from({ length: 10 }, (_, i) => ({
|
|
303
|
+
logicalId: `DriftedVPC${i}`,
|
|
304
|
+
physicalId: `vpc-${i}`,
|
|
305
|
+
resourceType: 'AWS::EC2::VPC',
|
|
306
|
+
state: ResourceState.DRIFTED,
|
|
307
|
+
})),
|
|
308
|
+
].map((r) => new Resource(r));
|
|
309
|
+
|
|
310
|
+
const issues = [
|
|
311
|
+
...Array.from({ length: 5 }, (_, i) =>
|
|
312
|
+
Issue.missingResource({
|
|
313
|
+
resourceType: 'AWS::Lambda::Function',
|
|
314
|
+
resourceId: `MissingLambda${i}`,
|
|
315
|
+
description: 'Missing Lambda',
|
|
316
|
+
})
|
|
317
|
+
),
|
|
318
|
+
...Array.from({ length: 5 }, (_, i) =>
|
|
319
|
+
Issue.propertyMismatch({
|
|
320
|
+
resourceType: 'AWS::Lambda::Function',
|
|
321
|
+
resourceId: `lambda-${i}`,
|
|
322
|
+
mismatch: new PropertyMismatch({
|
|
323
|
+
propertyPath: 'Environment',
|
|
324
|
+
expectedValue: 'expected',
|
|
325
|
+
actualValue: 'actual',
|
|
326
|
+
mutability: PropertyMutability.MUTABLE,
|
|
327
|
+
}),
|
|
328
|
+
})
|
|
329
|
+
),
|
|
330
|
+
...Array.from({ length: 10 }, (_, i) =>
|
|
331
|
+
Issue.propertyMismatch({
|
|
332
|
+
resourceType: 'AWS::EC2::VPC',
|
|
333
|
+
resourceId: `vpc-${i}`,
|
|
334
|
+
mismatch: new PropertyMismatch({
|
|
335
|
+
propertyPath: 'Tags',
|
|
336
|
+
expectedValue: 'expected',
|
|
337
|
+
actualValue: 'actual',
|
|
338
|
+
mutability: PropertyMutability.MUTABLE,
|
|
339
|
+
}),
|
|
340
|
+
})
|
|
341
|
+
),
|
|
342
|
+
];
|
|
343
|
+
|
|
344
|
+
const score = calculator.calculate({ resources, issues });
|
|
345
|
+
|
|
346
|
+
// Critical impact: 5/20 = 25% → penalty: 25% × 50 = 12.5 points
|
|
347
|
+
// Functional drift: 5/10 = 50% → penalty: 50% × 30 = 15 points
|
|
348
|
+
// Infra drift: 10/10 = 100% → penalty: 100% × 20 = 20 points
|
|
349
|
+
// Total penalty: 12.5 + 15 + 20 = 47.5 points
|
|
350
|
+
// Score: 100 - 47.5 = 52.5 (rounded to 53)
|
|
351
|
+
expect(score.value).toBeGreaterThanOrEqual(52);
|
|
352
|
+
expect(score.value).toBeLessThanOrEqual(53);
|
|
353
|
+
expect(score.isHealthy()).toBe(false); // Below 80 threshold
|
|
354
|
+
expect(score.isDegraded()).toBe(true); // 40-79 is degraded
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
describe('Edge cases', () => {
|
|
359
|
+
test('should handle stack with no resources gracefully', () => {
|
|
360
|
+
const score = calculator.calculate({ resources: [], issues: [] });
|
|
361
|
+
|
|
362
|
+
// No resources, no issues = perfect score
|
|
363
|
+
expect(score.value).toBe(100);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
test('should handle stack with no issues', () => {
|
|
367
|
+
const resources = Array.from({ length: 10 }, (_, i) => ({
|
|
368
|
+
logicalId: `Resource${i}`,
|
|
369
|
+
physicalId: `resource-${i}`,
|
|
370
|
+
resourceType: 'AWS::Lambda::Function',
|
|
371
|
+
state: ResourceState.IN_STACK,
|
|
372
|
+
})).map((r) => new Resource(r));
|
|
373
|
+
|
|
374
|
+
const score = calculator.calculate({ resources, issues: [] });
|
|
375
|
+
|
|
376
|
+
// No issues = perfect score
|
|
377
|
+
expect(score.value).toBe(100);
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
});
|