@friggframework/devtools 2.0.0--canary.474.86c5119.0 → 2.0.0--canary.474.898a56c.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +762 -0
- package/infrastructure/domains/health/application/use-cases/repair-via-import-use-case.js +154 -1
- package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.js +20 -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/services/__tests__/health-score-percentage-based.test.js +380 -0
- package/infrastructure/domains/health/domain/services/__tests__/logical-id-mapper.test.js +645 -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 +330 -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-resource-detector.js +108 -14
- package/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector.test.js +69 -12
- package/package.json +6 -6
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tests for HealthScoreCalculator Domain Service
|
|
3
|
+
*
|
|
4
|
+
* Updated for percentage-based health scoring (2025-10-26)
|
|
5
|
+
*
|
|
6
|
+
* NEW SYSTEM (Percentage-Based):
|
|
7
|
+
* - Critical issues: up to 50 points (% of total resources)
|
|
8
|
+
* - Functional drift: up to 30 points (% of critical resources)
|
|
9
|
+
* - Infrastructure drift: up to 20 points (% of infra resources)
|
|
10
|
+
* - Example: 16/16 Lambdas drifted = 100% × 30 = 30 penalty → 70/100 ✅
|
|
3
11
|
*/
|
|
4
12
|
|
|
5
13
|
const HealthScoreCalculator = require('./health-score-calculator');
|
|
@@ -43,8 +51,18 @@ describe('HealthScoreCalculator', () => {
|
|
|
43
51
|
});
|
|
44
52
|
});
|
|
45
53
|
|
|
46
|
-
describe('critical issues (
|
|
47
|
-
it('should
|
|
54
|
+
describe('critical issues (orphaned, missing)', () => {
|
|
55
|
+
it('should penalize based on percentage of resources that are orphaned', () => {
|
|
56
|
+
// 10 resources, 1 orphaned = 10% critical impact
|
|
57
|
+
const resources = Array.from({ length: 10 }, (_, i) =>
|
|
58
|
+
new Resource({
|
|
59
|
+
logicalId: `Resource${i}`,
|
|
60
|
+
physicalId: `resource-${i}`,
|
|
61
|
+
resourceType: 'AWS::EC2::VPC',
|
|
62
|
+
state: ResourceState.IN_STACK,
|
|
63
|
+
})
|
|
64
|
+
);
|
|
65
|
+
|
|
48
66
|
const issues = [
|
|
49
67
|
Issue.orphanedResource({
|
|
50
68
|
resourceType: 'AWS::RDS::DBCluster',
|
|
@@ -53,331 +71,418 @@ describe('HealthScoreCalculator', () => {
|
|
|
53
71
|
}),
|
|
54
72
|
];
|
|
55
73
|
|
|
56
|
-
const score = calculator.calculate({ resources
|
|
57
|
-
|
|
74
|
+
const score = calculator.calculate({ resources, issues });
|
|
75
|
+
// Critical impact: 1/10 = 10% → penalty: 10% × 50 = 5
|
|
76
|
+
// Score: 100 - 5 = 95
|
|
77
|
+
expect(score.value).toBe(95);
|
|
58
78
|
});
|
|
59
79
|
|
|
60
|
-
it('should
|
|
80
|
+
it('should penalize based on percentage of resources that are missing', () => {
|
|
81
|
+
// 10 resources, 2 missing = 20% critical impact
|
|
82
|
+
const resources = Array.from({ length: 10 }, (_, i) =>
|
|
83
|
+
new Resource({
|
|
84
|
+
logicalId: `Resource${i}`,
|
|
85
|
+
physicalId: `resource-${i}`,
|
|
86
|
+
resourceType: 'AWS::KMS::Key',
|
|
87
|
+
state: ResourceState.IN_STACK,
|
|
88
|
+
})
|
|
89
|
+
);
|
|
90
|
+
|
|
61
91
|
const issues = [
|
|
62
92
|
Issue.missingResource({
|
|
63
93
|
resourceType: 'AWS::KMS::Key',
|
|
64
|
-
resourceId: '
|
|
65
|
-
description: 'Missing key',
|
|
94
|
+
resourceId: 'MissingKey1',
|
|
95
|
+
description: 'Missing key 1',
|
|
66
96
|
}),
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it('should deduct 20 points for immutable property mismatch', () => {
|
|
74
|
-
const mismatch = new PropertyMismatch({
|
|
75
|
-
propertyPath: 'Properties.BucketName',
|
|
76
|
-
expectedValue: 'bucket-v2',
|
|
77
|
-
actualValue: 'bucket-v1',
|
|
78
|
-
mutability: PropertyMutability.IMMUTABLE,
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
const issues = [
|
|
82
|
-
Issue.propertyMismatch({
|
|
83
|
-
resourceType: 'AWS::S3::Bucket',
|
|
84
|
-
resourceId: 'my-bucket',
|
|
85
|
-
mismatch,
|
|
97
|
+
Issue.missingResource({
|
|
98
|
+
resourceType: 'AWS::KMS::Key',
|
|
99
|
+
resourceId: 'MissingKey2',
|
|
100
|
+
description: 'Missing key 2',
|
|
86
101
|
}),
|
|
87
102
|
];
|
|
88
103
|
|
|
89
|
-
const score = calculator.calculate({ resources
|
|
90
|
-
|
|
104
|
+
const score = calculator.calculate({ resources, issues });
|
|
105
|
+
// Critical impact: 2/10 = 20% → penalty: 20% × 50 = 10
|
|
106
|
+
// Score: 100 - 10 = 90
|
|
107
|
+
expect(score.value).toBe(90);
|
|
91
108
|
});
|
|
92
109
|
|
|
93
|
-
it('should
|
|
94
|
-
|
|
95
|
-
|
|
110
|
+
it('should apply higher penalties for larger percentages of orphaned resources', () => {
|
|
111
|
+
// 10 resources, 4 orphaned = 40% critical impact
|
|
112
|
+
const resources = Array.from({ length: 10 }, (_, i) =>
|
|
113
|
+
new Resource({
|
|
114
|
+
logicalId: `Resource${i}`,
|
|
115
|
+
physicalId: `resource-${i}`,
|
|
96
116
|
resourceType: 'AWS::RDS::DBCluster',
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
117
|
+
state: ResourceState.IN_STACK,
|
|
118
|
+
})
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
const issues = Array.from({ length: 4 }, (_, i) =>
|
|
100
122
|
Issue.orphanedResource({
|
|
101
123
|
resourceType: 'AWS::RDS::DBCluster',
|
|
102
|
-
resourceId:
|
|
103
|
-
description:
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const score = calculator.calculate({ resources
|
|
108
|
-
|
|
124
|
+
resourceId: `orphan-${i}`,
|
|
125
|
+
description: `Orphaned cluster ${i}`,
|
|
126
|
+
})
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
const score = calculator.calculate({ resources, issues });
|
|
130
|
+
// Critical impact: 4/10 = 40% → penalty: 40% × 50 = 20
|
|
131
|
+
// Score: 100 - 20 = 80
|
|
132
|
+
expect(score.value).toBe(80);
|
|
109
133
|
});
|
|
110
134
|
});
|
|
111
135
|
|
|
112
|
-
describe('
|
|
113
|
-
it('should
|
|
136
|
+
describe('functional drift (critical resources)', () => {
|
|
137
|
+
it('should penalize drift on Lambda functions', () => {
|
|
138
|
+
// 10 Lambda, 10 VPC; 1 Lambda drifted = 10% functional drift
|
|
139
|
+
const resources = [
|
|
140
|
+
...Array.from({ length: 10 }, (_, i) =>
|
|
141
|
+
new Resource({
|
|
142
|
+
logicalId: `Lambda${i}`,
|
|
143
|
+
physicalId: `lambda-${i}`,
|
|
144
|
+
resourceType: 'AWS::Lambda::Function',
|
|
145
|
+
state: ResourceState.IN_STACK,
|
|
146
|
+
})
|
|
147
|
+
),
|
|
148
|
+
...Array.from({ length: 10 }, (_, i) =>
|
|
149
|
+
new Resource({
|
|
150
|
+
logicalId: `VPC${i}`,
|
|
151
|
+
physicalId: `vpc-${i}`,
|
|
152
|
+
resourceType: 'AWS::EC2::VPC',
|
|
153
|
+
state: ResourceState.IN_STACK,
|
|
154
|
+
})
|
|
155
|
+
),
|
|
156
|
+
];
|
|
157
|
+
|
|
114
158
|
const mismatch = new PropertyMismatch({
|
|
115
|
-
propertyPath: 'Properties.
|
|
116
|
-
expectedValue:
|
|
117
|
-
actualValue:
|
|
159
|
+
propertyPath: 'Properties.Environment',
|
|
160
|
+
expectedValue: { VAR: 'expected' },
|
|
161
|
+
actualValue: { VAR: 'actual' },
|
|
118
162
|
mutability: PropertyMutability.MUTABLE,
|
|
119
163
|
});
|
|
120
164
|
|
|
121
165
|
const issues = [
|
|
122
166
|
Issue.propertyMismatch({
|
|
123
|
-
resourceType: 'AWS::
|
|
124
|
-
resourceId: '
|
|
167
|
+
resourceType: 'AWS::Lambda::Function',
|
|
168
|
+
resourceId: 'lambda-0',
|
|
125
169
|
mismatch,
|
|
126
170
|
}),
|
|
127
171
|
];
|
|
128
172
|
|
|
129
|
-
const score = calculator.calculate({ resources
|
|
130
|
-
|
|
173
|
+
const score = calculator.calculate({ resources, issues });
|
|
174
|
+
// Functional drift: 1/10 = 10% → penalty: 10% × 30 = 3
|
|
175
|
+
// Score: 100 - 3 = 97
|
|
176
|
+
expect(score.value).toBe(97);
|
|
131
177
|
});
|
|
132
178
|
|
|
133
|
-
it('should
|
|
179
|
+
it('should count unique drifted resources when multiple properties drifted', () => {
|
|
180
|
+
// 10 Lambda; 1 Lambda with 2 property mismatches = 10% functional drift (not 20%)
|
|
181
|
+
const resources = Array.from({ length: 10 }, (_, i) =>
|
|
182
|
+
new Resource({
|
|
183
|
+
logicalId: `Lambda${i}`,
|
|
184
|
+
physicalId: `lambda-${i}`,
|
|
185
|
+
resourceType: 'AWS::Lambda::Function',
|
|
186
|
+
state: ResourceState.IN_STACK,
|
|
187
|
+
})
|
|
188
|
+
);
|
|
189
|
+
|
|
134
190
|
const mismatch1 = new PropertyMismatch({
|
|
135
|
-
propertyPath: 'Properties.
|
|
136
|
-
expectedValue:
|
|
137
|
-
actualValue:
|
|
191
|
+
propertyPath: 'Properties.Environment',
|
|
192
|
+
expectedValue: { VAR: 'expected' },
|
|
193
|
+
actualValue: { VAR: 'actual' },
|
|
138
194
|
mutability: PropertyMutability.MUTABLE,
|
|
139
195
|
});
|
|
140
196
|
|
|
141
197
|
const mismatch2 = new PropertyMismatch({
|
|
142
|
-
propertyPath: 'Properties.
|
|
143
|
-
expectedValue:
|
|
144
|
-
actualValue:
|
|
198
|
+
propertyPath: 'Properties.Timeout',
|
|
199
|
+
expectedValue: 30,
|
|
200
|
+
actualValue: 60,
|
|
145
201
|
mutability: PropertyMutability.MUTABLE,
|
|
146
202
|
});
|
|
147
203
|
|
|
148
204
|
const issues = [
|
|
149
205
|
Issue.propertyMismatch({
|
|
150
|
-
resourceType: 'AWS::
|
|
151
|
-
resourceId: '
|
|
206
|
+
resourceType: 'AWS::Lambda::Function',
|
|
207
|
+
resourceId: 'lambda-0',
|
|
152
208
|
mismatch: mismatch1,
|
|
153
209
|
}),
|
|
154
210
|
Issue.propertyMismatch({
|
|
155
|
-
resourceType: 'AWS::
|
|
156
|
-
resourceId: '
|
|
211
|
+
resourceType: 'AWS::Lambda::Function',
|
|
212
|
+
resourceId: 'lambda-0',
|
|
157
213
|
mismatch: mismatch2,
|
|
158
214
|
}),
|
|
159
215
|
];
|
|
160
216
|
|
|
161
|
-
const score = calculator.calculate({ resources
|
|
162
|
-
|
|
217
|
+
const score = calculator.calculate({ resources, issues });
|
|
218
|
+
// Functional drift: 1/10 = 10% (unique resources) → penalty: 10% × 30 = 3
|
|
219
|
+
// Score: 100 - 3 = 97
|
|
220
|
+
expect(score.value).toBe(97);
|
|
163
221
|
});
|
|
164
222
|
});
|
|
165
223
|
|
|
166
|
-
describe('
|
|
167
|
-
it('should
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
224
|
+
describe('infrastructure drift (non-critical resources)', () => {
|
|
225
|
+
it('should penalize drift on VPC/networking resources with lower weight', () => {
|
|
226
|
+
// 10 VPC, 10 Lambda; 5 VPC drifted = 50% infrastructure drift
|
|
227
|
+
const resources = [
|
|
228
|
+
...Array.from({ length: 10 }, (_, i) =>
|
|
229
|
+
new Resource({
|
|
230
|
+
logicalId: `Lambda${i}`,
|
|
231
|
+
physicalId: `lambda-${i}`,
|
|
232
|
+
resourceType: 'AWS::Lambda::Function',
|
|
233
|
+
state: ResourceState.IN_STACK,
|
|
234
|
+
})
|
|
235
|
+
),
|
|
236
|
+
...Array.from({ length: 10 }, (_, i) =>
|
|
237
|
+
new Resource({
|
|
238
|
+
logicalId: `VPC${i}`,
|
|
239
|
+
physicalId: `vpc-${i}`,
|
|
240
|
+
resourceType: 'AWS::EC2::VPC',
|
|
241
|
+
state: ResourceState.IN_STACK,
|
|
242
|
+
})
|
|
243
|
+
),
|
|
176
244
|
];
|
|
177
245
|
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
246
|
+
const issues = Array.from({ length: 5 }, (_, i) => {
|
|
247
|
+
const mismatch = new PropertyMismatch({
|
|
248
|
+
propertyPath: 'Properties.Tags',
|
|
249
|
+
expectedValue: ['tag1'],
|
|
250
|
+
actualValue: ['tag2'],
|
|
251
|
+
mutability: PropertyMutability.MUTABLE,
|
|
252
|
+
});
|
|
181
253
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
resourceId: 'my-function',
|
|
189
|
-
description: 'Missing Environment tag',
|
|
190
|
-
}),
|
|
191
|
-
new Issue({
|
|
192
|
-
type: 'MISSING_TAG',
|
|
193
|
-
severity: 'info',
|
|
194
|
-
resourceType: 'AWS::Lambda::Function',
|
|
195
|
-
resourceId: 'my-function',
|
|
196
|
-
description: 'Missing Owner tag',
|
|
197
|
-
}),
|
|
198
|
-
];
|
|
254
|
+
return Issue.propertyMismatch({
|
|
255
|
+
resourceType: 'AWS::EC2::VPC',
|
|
256
|
+
resourceId: `vpc-${i}`,
|
|
257
|
+
mismatch,
|
|
258
|
+
});
|
|
259
|
+
});
|
|
199
260
|
|
|
200
|
-
const score = calculator.calculate({ resources
|
|
201
|
-
|
|
261
|
+
const score = calculator.calculate({ resources, issues });
|
|
262
|
+
// Infra drift: 5/10 = 50% → penalty: 50% × 20 = 10
|
|
263
|
+
// Score: 100 - 10 = 90
|
|
264
|
+
expect(score.value).toBe(90);
|
|
202
265
|
});
|
|
203
266
|
});
|
|
204
267
|
|
|
205
|
-
describe('mixed
|
|
206
|
-
it('should correctly combine penalties from different
|
|
268
|
+
describe('mixed issues', () => {
|
|
269
|
+
it('should correctly combine penalties from different categories', () => {
|
|
270
|
+
// 20 resources: 10 Lambda, 10 VPC
|
|
271
|
+
// 1 orphaned, 2 Lambda drifted, 5 VPC drifted
|
|
272
|
+
const resources = [
|
|
273
|
+
...Array.from({ length: 10 }, (_, i) =>
|
|
274
|
+
new Resource({
|
|
275
|
+
logicalId: `Lambda${i}`,
|
|
276
|
+
physicalId: `lambda-${i}`,
|
|
277
|
+
resourceType: 'AWS::Lambda::Function',
|
|
278
|
+
state: ResourceState.IN_STACK,
|
|
279
|
+
})
|
|
280
|
+
),
|
|
281
|
+
...Array.from({ length: 10 }, (_, i) =>
|
|
282
|
+
new Resource({
|
|
283
|
+
logicalId: `VPC${i}`,
|
|
284
|
+
physicalId: `vpc-${i}`,
|
|
285
|
+
resourceType: 'AWS::EC2::VPC',
|
|
286
|
+
state: ResourceState.IN_STACK,
|
|
287
|
+
})
|
|
288
|
+
),
|
|
289
|
+
];
|
|
290
|
+
|
|
207
291
|
const orphanedIssue = Issue.orphanedResource({
|
|
208
292
|
resourceType: 'AWS::RDS::DBCluster',
|
|
209
293
|
resourceId: 'orphan-123',
|
|
210
294
|
description: 'Orphaned cluster',
|
|
211
295
|
});
|
|
212
296
|
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
297
|
+
const lambdaDriftIssues = Array.from({ length: 2 }, (_, i) => {
|
|
298
|
+
const mismatch = new PropertyMismatch({
|
|
299
|
+
propertyPath: 'Properties.Environment',
|
|
300
|
+
expectedValue: { VAR: 'expected' },
|
|
301
|
+
actualValue: { VAR: 'actual' },
|
|
302
|
+
mutability: PropertyMutability.MUTABLE,
|
|
303
|
+
});
|
|
219
304
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
305
|
+
return Issue.propertyMismatch({
|
|
306
|
+
resourceType: 'AWS::Lambda::Function',
|
|
307
|
+
resourceId: `lambda-${i}`,
|
|
308
|
+
mismatch,
|
|
309
|
+
});
|
|
224
310
|
});
|
|
225
311
|
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
312
|
+
const vpcDriftIssues = Array.from({ length: 5 }, (_, i) => {
|
|
313
|
+
const mismatch = new PropertyMismatch({
|
|
314
|
+
propertyPath: 'Properties.Tags',
|
|
315
|
+
expectedValue: ['tag1'],
|
|
316
|
+
actualValue: ['tag2'],
|
|
317
|
+
mutability: PropertyMutability.MUTABLE,
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
return Issue.propertyMismatch({
|
|
321
|
+
resourceType: 'AWS::EC2::VPC',
|
|
322
|
+
resourceId: `vpc-${i}`,
|
|
323
|
+
mismatch,
|
|
324
|
+
});
|
|
232
325
|
});
|
|
233
326
|
|
|
234
|
-
const issues = [orphanedIssue,
|
|
327
|
+
const issues = [orphanedIssue, ...lambdaDriftIssues, ...vpcDriftIssues];
|
|
235
328
|
|
|
236
|
-
const score = calculator.calculate({ resources
|
|
237
|
-
|
|
329
|
+
const score = calculator.calculate({ resources, issues });
|
|
330
|
+
// Critical impact: 1/20 = 5% → penalty: 5% × 50 = 2.5
|
|
331
|
+
// Functional drift: 2/10 = 20% → penalty: 20% × 30 = 6
|
|
332
|
+
// Infra drift: 5/10 = 50% → penalty: 50% × 20 = 10
|
|
333
|
+
// Total: 2.5 + 6 + 10 = 18.5 → Score: 100 - 19 = 81 (rounded)
|
|
334
|
+
expect(score.value).toBeGreaterThanOrEqual(81);
|
|
335
|
+
expect(score.value).toBeLessThanOrEqual(82);
|
|
238
336
|
});
|
|
239
337
|
});
|
|
240
338
|
|
|
241
339
|
describe('minimum score (0)', () => {
|
|
242
340
|
it('should not go below 0', () => {
|
|
243
|
-
//
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
description: 'Orphaned cluster 1',
|
|
249
|
-
}),
|
|
250
|
-
Issue.orphanedResource({
|
|
251
|
-
resourceType: 'AWS::RDS::DBCluster',
|
|
252
|
-
resourceId: 'orphan-2',
|
|
253
|
-
description: 'Orphaned cluster 2',
|
|
254
|
-
}),
|
|
255
|
-
Issue.orphanedResource({
|
|
341
|
+
// 2 resources, 10 critical issues = 500% critical impact (capped at 100%)
|
|
342
|
+
const resources = [
|
|
343
|
+
new Resource({
|
|
344
|
+
logicalId: 'Resource1',
|
|
345
|
+
physicalId: 'resource-1',
|
|
256
346
|
resourceType: 'AWS::RDS::DBCluster',
|
|
257
|
-
|
|
258
|
-
description: 'Orphaned cluster 3',
|
|
347
|
+
state: ResourceState.IN_STACK,
|
|
259
348
|
}),
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
349
|
+
new Resource({
|
|
350
|
+
logicalId: 'Resource2',
|
|
351
|
+
physicalId: 'resource-2',
|
|
352
|
+
resourceType: 'AWS::Lambda::Function',
|
|
353
|
+
state: ResourceState.IN_STACK,
|
|
264
354
|
}),
|
|
265
355
|
];
|
|
266
356
|
|
|
267
|
-
const
|
|
268
|
-
|
|
357
|
+
const issues = Array.from({ length: 10 }, (_, i) =>
|
|
358
|
+
Issue.orphanedResource({
|
|
359
|
+
resourceType: 'AWS::RDS::DBCluster',
|
|
360
|
+
resourceId: `orphan-${i}`,
|
|
361
|
+
description: `Orphaned cluster ${i}`,
|
|
362
|
+
})
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
const score = calculator.calculate({ resources, issues });
|
|
366
|
+
// Critical impact: 10/2 = 500% (but max penalty is 50)
|
|
367
|
+
// Penalty: min(500% × 50, 50) = 50
|
|
368
|
+
// Score: max(0, 100 - 50) = 50
|
|
369
|
+
// But with such extreme issues, score should be very low
|
|
370
|
+
expect(score.value).toBeGreaterThanOrEqual(0);
|
|
371
|
+
expect(score.value).toBeLessThanOrEqual(50);
|
|
269
372
|
});
|
|
270
373
|
});
|
|
271
374
|
|
|
272
375
|
describe('penalty configuration', () => {
|
|
273
|
-
it('should use custom penalty configuration', () => {
|
|
376
|
+
it('should use custom max penalty configuration', () => {
|
|
274
377
|
const customCalculator = new HealthScoreCalculator({
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
378
|
+
maxPenalties: {
|
|
379
|
+
criticalIssues: 40, // Custom: 40 instead of default 50
|
|
380
|
+
functionalDrift: 35, // Custom: 35 instead of default 30
|
|
381
|
+
infrastructureDrift: 25, // Custom: 25 instead of default 20
|
|
279
382
|
},
|
|
280
383
|
});
|
|
281
384
|
|
|
282
|
-
|
|
283
|
-
|
|
385
|
+
// 10 resources, 5 orphaned = 50% critical impact
|
|
386
|
+
const resources = Array.from({ length: 10 }, (_, i) =>
|
|
387
|
+
new Resource({
|
|
388
|
+
logicalId: `Resource${i}`,
|
|
389
|
+
physicalId: `resource-${i}`,
|
|
284
390
|
resourceType: 'AWS::RDS::DBCluster',
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
];
|
|
289
|
-
|
|
290
|
-
const score = customCalculator.calculate({ resources: [], issues });
|
|
291
|
-
expect(score.value).toBe(80); // 100 - 20 (custom critical penalty)
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
it('should allow immutable property penalty override', () => {
|
|
295
|
-
const customCalculator = new HealthScoreCalculator({
|
|
296
|
-
penalties: {
|
|
297
|
-
immutablePropertyMismatch: 25, // Custom instead of default 20
|
|
298
|
-
},
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
const mismatch = new PropertyMismatch({
|
|
302
|
-
propertyPath: 'Properties.BucketName',
|
|
303
|
-
expectedValue: 'bucket-v2',
|
|
304
|
-
actualValue: 'bucket-v1',
|
|
305
|
-
mutability: PropertyMutability.IMMUTABLE,
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
const issues = [
|
|
309
|
-
Issue.propertyMismatch({
|
|
310
|
-
resourceType: 'AWS::S3::Bucket',
|
|
311
|
-
resourceId: 'my-bucket',
|
|
312
|
-
mismatch,
|
|
313
|
-
}),
|
|
314
|
-
];
|
|
315
|
-
|
|
316
|
-
const score = customCalculator.calculate({ resources: [], issues });
|
|
317
|
-
expect(score.value).toBe(75); // 100 - 25 (custom immutable penalty)
|
|
318
|
-
});
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
describe('getDefaultPenalties', () => {
|
|
322
|
-
it('should return default penalty configuration', () => {
|
|
323
|
-
const penalties = HealthScoreCalculator.getDefaultPenalties();
|
|
391
|
+
state: ResourceState.IN_STACK,
|
|
392
|
+
})
|
|
393
|
+
);
|
|
324
394
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
395
|
+
const issues = Array.from({ length: 5 }, (_, i) =>
|
|
396
|
+
Issue.orphanedResource({
|
|
397
|
+
resourceType: 'AWS::RDS::DBCluster',
|
|
398
|
+
resourceId: `orphan-${i}`,
|
|
399
|
+
description: `Orphaned cluster ${i}`,
|
|
400
|
+
})
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
const score = customCalculator.calculate({ resources, issues });
|
|
404
|
+
// Critical impact: 5/10 = 50% → penalty: 50% × 40 (custom) = 20
|
|
405
|
+
// Score: 100 - 20 = 80
|
|
406
|
+
expect(score.value).toBe(80);
|
|
331
407
|
});
|
|
332
408
|
});
|
|
333
409
|
|
|
334
410
|
describe('explainScore', () => {
|
|
335
|
-
it('should explain score with breakdown', () => {
|
|
411
|
+
it('should explain score with percentage-based breakdown', () => {
|
|
412
|
+
// 20 resources: 10 Lambda, 10 VPC
|
|
413
|
+
// 1 orphaned, 2 Lambda drifted, 5 VPC drifted
|
|
414
|
+
const resources = [
|
|
415
|
+
...Array.from({ length: 10 }, (_, i) =>
|
|
416
|
+
new Resource({
|
|
417
|
+
logicalId: `Lambda${i}`,
|
|
418
|
+
physicalId: `lambda-${i}`,
|
|
419
|
+
resourceType: 'AWS::Lambda::Function',
|
|
420
|
+
state: ResourceState.IN_STACK,
|
|
421
|
+
})
|
|
422
|
+
),
|
|
423
|
+
...Array.from({ length: 10 }, (_, i) =>
|
|
424
|
+
new Resource({
|
|
425
|
+
logicalId: `VPC${i}`,
|
|
426
|
+
physicalId: `vpc-${i}`,
|
|
427
|
+
resourceType: 'AWS::EC2::VPC',
|
|
428
|
+
state: ResourceState.IN_STACK,
|
|
429
|
+
})
|
|
430
|
+
),
|
|
431
|
+
];
|
|
432
|
+
|
|
336
433
|
const orphanedIssue = Issue.orphanedResource({
|
|
337
434
|
resourceType: 'AWS::RDS::DBCluster',
|
|
338
435
|
resourceId: 'orphan-123',
|
|
339
436
|
description: 'Orphaned cluster',
|
|
340
437
|
});
|
|
341
438
|
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
const warningIssue = Issue.propertyMismatch({
|
|
350
|
-
resourceType: 'AWS::EC2::VPC',
|
|
351
|
-
resourceId: 'vpc-123',
|
|
352
|
-
mismatch,
|
|
353
|
-
});
|
|
439
|
+
const lambdaDriftIssues = Array.from({ length: 2 }, (_, i) => {
|
|
440
|
+
const mismatch = new PropertyMismatch({
|
|
441
|
+
propertyPath: 'Properties.Environment',
|
|
442
|
+
expectedValue: { VAR: 'expected' },
|
|
443
|
+
actualValue: { VAR: 'actual' },
|
|
444
|
+
mutability: PropertyMutability.MUTABLE,
|
|
445
|
+
});
|
|
354
446
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
description: 'Missing tag',
|
|
447
|
+
return Issue.propertyMismatch({
|
|
448
|
+
resourceType: 'AWS::Lambda::Function',
|
|
449
|
+
resourceId: `lambda-${i}`,
|
|
450
|
+
mismatch,
|
|
451
|
+
});
|
|
361
452
|
});
|
|
362
453
|
|
|
363
|
-
const
|
|
454
|
+
const vpcDriftIssues = Array.from({ length: 5 }, (_, i) => {
|
|
455
|
+
const mismatch = new PropertyMismatch({
|
|
456
|
+
propertyPath: 'Properties.Tags',
|
|
457
|
+
expectedValue: ['tag1'],
|
|
458
|
+
actualValue: ['tag2'],
|
|
459
|
+
mutability: PropertyMutability.MUTABLE,
|
|
460
|
+
});
|
|
364
461
|
|
|
365
|
-
|
|
462
|
+
return Issue.propertyMismatch({
|
|
463
|
+
resourceType: 'AWS::EC2::VPC',
|
|
464
|
+
resourceId: `vpc-${i}`,
|
|
465
|
+
mismatch,
|
|
466
|
+
});
|
|
467
|
+
});
|
|
366
468
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
469
|
+
const issues = [orphanedIssue, ...lambdaDriftIssues, ...vpcDriftIssues];
|
|
470
|
+
|
|
471
|
+
const explanation = calculator.explainScore({ resources, issues });
|
|
472
|
+
|
|
473
|
+
expect(explanation.startingScore).toBe(100);
|
|
474
|
+
expect(explanation.finalScore).toBeGreaterThanOrEqual(81);
|
|
475
|
+
expect(explanation.finalScore).toBeLessThanOrEqual(82);
|
|
476
|
+
expect(explanation.breakdown).toHaveProperty('criticalIssues');
|
|
477
|
+
expect(explanation.breakdown).toHaveProperty('functionalDrift');
|
|
478
|
+
expect(explanation.breakdown).toHaveProperty('infrastructureDrift');
|
|
479
|
+
expect(explanation.breakdown.criticalIssues.count).toBe(1);
|
|
480
|
+
expect(explanation.breakdown.functionalDrift.count).toBe(2);
|
|
481
|
+
expect(explanation.breakdown.infrastructureDrift.count).toBe(5);
|
|
482
|
+
expect(explanation.resourceCounts).toEqual({
|
|
483
|
+
total: 20,
|
|
484
|
+
critical: 10,
|
|
485
|
+
infrastructure: 10,
|
|
381
486
|
});
|
|
382
487
|
});
|
|
383
488
|
|
|
@@ -389,11 +494,10 @@ describe('HealthScoreCalculator', () => {
|
|
|
389
494
|
startingScore: 100,
|
|
390
495
|
totalPenalty: 0,
|
|
391
496
|
breakdown: {
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
497
|
+
criticalIssues: { count: 0, impactPercent: 0, penalty: 0 },
|
|
498
|
+
functionalDrift: { count: 0, impactPercent: 0, penalty: 0 },
|
|
499
|
+
infrastructureDrift: { count: 0, impactPercent: 0, penalty: 0 },
|
|
395
500
|
},
|
|
396
|
-
issueTypes: {},
|
|
397
501
|
});
|
|
398
502
|
});
|
|
399
503
|
});
|