@friggframework/devtools 2.0.0--canary.474.82ba370.0 → 2.0.0--canary.474.d64c550.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/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/domain/entities/property-mismatch.js +7 -4
- package/infrastructure/domains/health/domain/entities/property-mismatch.test.js +28 -4
- 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/health-score-calculator.js +165 -0
- package/infrastructure/domains/health/domain/services/health-score-calculator.test.js +400 -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/value-objects/stack-identifier.js +13 -0
- package/infrastructure/domains/health/domain/value-objects/stack-identifier.test.js +29 -0
- package/package.json +6 -6
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HealthScoreCalculator Domain Service
|
|
3
|
+
*
|
|
4
|
+
* Calculates health scores (0-100) based on detected issues and resource states.
|
|
5
|
+
*
|
|
6
|
+
* Penalty System:
|
|
7
|
+
* - Critical issues (orphaned, missing, immutable drift): 20-30 points each
|
|
8
|
+
* - Warning issues (mutable drift): 10 points each
|
|
9
|
+
* - Info issues (missing tags): 5 points each
|
|
10
|
+
*
|
|
11
|
+
* Score is capped at 0 (cannot go negative).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const HealthScore = require('../value-objects/health-score');
|
|
15
|
+
|
|
16
|
+
class HealthScoreCalculator {
|
|
17
|
+
/**
|
|
18
|
+
* Default penalty values for different issue severities
|
|
19
|
+
* @private
|
|
20
|
+
*/
|
|
21
|
+
static DEFAULT_PENALTIES = {
|
|
22
|
+
critical: 30, // Orphaned resources, missing resources
|
|
23
|
+
warning: 10, // Mutable property mismatches
|
|
24
|
+
info: 5, // Missing tags, minor issues
|
|
25
|
+
immutablePropertyMismatch: 20, // Immutable property changes (requires replacement)
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Create a new HealthScoreCalculator
|
|
30
|
+
*
|
|
31
|
+
* @param {Object} [config={}]
|
|
32
|
+
* @param {Object} [config.penalties] - Custom penalty configuration
|
|
33
|
+
* @param {number} [config.penalties.critical] - Penalty for critical issues (default: 30)
|
|
34
|
+
* @param {number} [config.penalties.warning] - Penalty for warning issues (default: 10)
|
|
35
|
+
* @param {number} [config.penalties.info] - Penalty for info issues (default: 5)
|
|
36
|
+
* @param {number} [config.penalties.immutablePropertyMismatch] - Penalty for immutable property changes (default: 20)
|
|
37
|
+
*/
|
|
38
|
+
constructor(config = {}) {
|
|
39
|
+
this.penalties = {
|
|
40
|
+
...HealthScoreCalculator.DEFAULT_PENALTIES,
|
|
41
|
+
...(config.penalties || {}),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get default penalty configuration
|
|
47
|
+
* @returns {Object}
|
|
48
|
+
*/
|
|
49
|
+
static getDefaultPenalties() {
|
|
50
|
+
return { ...HealthScoreCalculator.DEFAULT_PENALTIES };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Calculate health score based on resources and issues
|
|
55
|
+
*
|
|
56
|
+
* @param {Object} params
|
|
57
|
+
* @param {Resource[]} params.resources - Resources in the stack
|
|
58
|
+
* @param {Issue[]} params.issues - Detected issues
|
|
59
|
+
* @returns {HealthScore}
|
|
60
|
+
*/
|
|
61
|
+
calculate({ resources, issues }) {
|
|
62
|
+
const startingScore = 100;
|
|
63
|
+
let totalPenalty = 0;
|
|
64
|
+
|
|
65
|
+
// Calculate penalties for each issue
|
|
66
|
+
for (const issue of issues) {
|
|
67
|
+
totalPenalty += this._calculateIssuePenalty(issue);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Calculate final score (capped at 0)
|
|
71
|
+
const finalScore = Math.max(0, startingScore - totalPenalty);
|
|
72
|
+
|
|
73
|
+
return new HealthScore(finalScore);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Calculate penalty for a single issue
|
|
78
|
+
*
|
|
79
|
+
* @private
|
|
80
|
+
* @param {Issue} issue
|
|
81
|
+
* @returns {number}
|
|
82
|
+
*/
|
|
83
|
+
_calculateIssuePenalty(issue) {
|
|
84
|
+
// Special case: immutable property mismatch has higher penalty than regular critical
|
|
85
|
+
if (
|
|
86
|
+
issue.isPropertyMismatch() &&
|
|
87
|
+
issue.propertyMismatch &&
|
|
88
|
+
issue.propertyMismatch.requiresReplacement()
|
|
89
|
+
) {
|
|
90
|
+
return this.penalties.immutablePropertyMismatch;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Standard severity-based penalties
|
|
94
|
+
if (issue.isCritical()) {
|
|
95
|
+
return this.penalties.critical;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (issue.isWarning()) {
|
|
99
|
+
return this.penalties.warning;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (issue.isInfo()) {
|
|
103
|
+
return this.penalties.info;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Fallback (should never reach here if Issue validation is correct)
|
|
107
|
+
return 0;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Explain the score calculation with detailed breakdown
|
|
112
|
+
*
|
|
113
|
+
* @param {Object} params
|
|
114
|
+
* @param {Resource[]} params.resources - Resources in the stack
|
|
115
|
+
* @param {Issue[]} params.issues - Detected issues
|
|
116
|
+
* @returns {Object} Explanation with breakdown
|
|
117
|
+
*/
|
|
118
|
+
explainScore({ resources, issues }) {
|
|
119
|
+
const startingScore = 100;
|
|
120
|
+
let totalPenalty = 0;
|
|
121
|
+
|
|
122
|
+
// Count issues by severity
|
|
123
|
+
const breakdown = {
|
|
124
|
+
critical: { count: 0, penalty: 0 },
|
|
125
|
+
warning: { count: 0, penalty: 0 },
|
|
126
|
+
info: { count: 0, penalty: 0 },
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// Count issues by type
|
|
130
|
+
const issueTypes = {};
|
|
131
|
+
|
|
132
|
+
// Process each issue
|
|
133
|
+
for (const issue of issues) {
|
|
134
|
+
const penalty = this._calculateIssuePenalty(issue);
|
|
135
|
+
totalPenalty += penalty;
|
|
136
|
+
|
|
137
|
+
// Track by severity
|
|
138
|
+
if (issue.isCritical()) {
|
|
139
|
+
breakdown.critical.count++;
|
|
140
|
+
breakdown.critical.penalty += penalty;
|
|
141
|
+
} else if (issue.isWarning()) {
|
|
142
|
+
breakdown.warning.count++;
|
|
143
|
+
breakdown.warning.penalty += penalty;
|
|
144
|
+
} else if (issue.isInfo()) {
|
|
145
|
+
breakdown.info.count++;
|
|
146
|
+
breakdown.info.penalty += penalty;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Track by type
|
|
150
|
+
issueTypes[issue.type] = (issueTypes[issue.type] || 0) + 1;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const finalScore = Math.max(0, startingScore - totalPenalty);
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
finalScore,
|
|
157
|
+
startingScore,
|
|
158
|
+
totalPenalty,
|
|
159
|
+
breakdown,
|
|
160
|
+
issueTypes,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
module.exports = HealthScoreCalculator;
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for HealthScoreCalculator Domain Service
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const HealthScoreCalculator = require('./health-score-calculator');
|
|
6
|
+
const Resource = require('../entities/resource');
|
|
7
|
+
const Issue = require('../entities/issue');
|
|
8
|
+
const ResourceState = require('../value-objects/resource-state');
|
|
9
|
+
const PropertyMutability = require('../value-objects/property-mutability');
|
|
10
|
+
const PropertyMismatch = require('../entities/property-mismatch');
|
|
11
|
+
|
|
12
|
+
describe('HealthScoreCalculator', () => {
|
|
13
|
+
let calculator;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
calculator = new HealthScoreCalculator();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('perfect health (100)', () => {
|
|
20
|
+
it('should return 100 for no resources and no issues', () => {
|
|
21
|
+
const score = calculator.calculate({ resources: [], issues: [] });
|
|
22
|
+
expect(score.value).toBe(100);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should return 100 for all healthy resources with no issues', () => {
|
|
26
|
+
const resources = [
|
|
27
|
+
new Resource({
|
|
28
|
+
logicalId: 'VPC',
|
|
29
|
+
physicalId: 'vpc-123',
|
|
30
|
+
resourceType: 'AWS::EC2::VPC',
|
|
31
|
+
state: ResourceState.IN_STACK,
|
|
32
|
+
}),
|
|
33
|
+
new Resource({
|
|
34
|
+
logicalId: 'Subnet',
|
|
35
|
+
physicalId: 'subnet-123',
|
|
36
|
+
resourceType: 'AWS::EC2::Subnet',
|
|
37
|
+
state: ResourceState.IN_STACK,
|
|
38
|
+
}),
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
const score = calculator.calculate({ resources, issues: [] });
|
|
42
|
+
expect(score.value).toBe(100);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('critical issues (heavy penalties)', () => {
|
|
47
|
+
it('should deduct 30 points for orphaned resource', () => {
|
|
48
|
+
const issues = [
|
|
49
|
+
Issue.orphanedResource({
|
|
50
|
+
resourceType: 'AWS::RDS::DBCluster',
|
|
51
|
+
resourceId: 'orphan-123',
|
|
52
|
+
description: 'Orphaned cluster',
|
|
53
|
+
}),
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
const score = calculator.calculate({ resources: [], issues });
|
|
57
|
+
expect(score.value).toBe(70); // 100 - 30
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should deduct 30 points for missing resource', () => {
|
|
61
|
+
const issues = [
|
|
62
|
+
Issue.missingResource({
|
|
63
|
+
resourceType: 'AWS::KMS::Key',
|
|
64
|
+
resourceId: 'MissingKey',
|
|
65
|
+
description: 'Missing key',
|
|
66
|
+
}),
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
const score = calculator.calculate({ resources: [], issues });
|
|
70
|
+
expect(score.value).toBe(70); // 100 - 30
|
|
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,
|
|
86
|
+
}),
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
const score = calculator.calculate({ resources: [], issues });
|
|
90
|
+
expect(score.value).toBe(80); // 100 - 20
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should deduct 60 points for two orphaned resources', () => {
|
|
94
|
+
const issues = [
|
|
95
|
+
Issue.orphanedResource({
|
|
96
|
+
resourceType: 'AWS::RDS::DBCluster',
|
|
97
|
+
resourceId: 'orphan-1',
|
|
98
|
+
description: 'Orphaned cluster 1',
|
|
99
|
+
}),
|
|
100
|
+
Issue.orphanedResource({
|
|
101
|
+
resourceType: 'AWS::RDS::DBCluster',
|
|
102
|
+
resourceId: 'orphan-2',
|
|
103
|
+
description: 'Orphaned cluster 2',
|
|
104
|
+
}),
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
const score = calculator.calculate({ resources: [], issues });
|
|
108
|
+
expect(score.value).toBe(40); // 100 - 30 - 30
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe('warning issues (moderate penalties)', () => {
|
|
113
|
+
it('should deduct 10 points for mutable property mismatch', () => {
|
|
114
|
+
const mismatch = new PropertyMismatch({
|
|
115
|
+
propertyPath: 'Properties.Tags',
|
|
116
|
+
expectedValue: ['tag1'],
|
|
117
|
+
actualValue: ['tag2'],
|
|
118
|
+
mutability: PropertyMutability.MUTABLE,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const issues = [
|
|
122
|
+
Issue.propertyMismatch({
|
|
123
|
+
resourceType: 'AWS::EC2::VPC',
|
|
124
|
+
resourceId: 'vpc-123',
|
|
125
|
+
mismatch,
|
|
126
|
+
}),
|
|
127
|
+
];
|
|
128
|
+
|
|
129
|
+
const score = calculator.calculate({ resources: [], issues });
|
|
130
|
+
expect(score.value).toBe(90); // 100 - 10
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should deduct 20 points for two mutable property mismatches', () => {
|
|
134
|
+
const mismatch1 = new PropertyMismatch({
|
|
135
|
+
propertyPath: 'Properties.Tags',
|
|
136
|
+
expectedValue: ['tag1'],
|
|
137
|
+
actualValue: ['tag2'],
|
|
138
|
+
mutability: PropertyMutability.MUTABLE,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const mismatch2 = new PropertyMismatch({
|
|
142
|
+
propertyPath: 'Properties.EnableDnsSupport',
|
|
143
|
+
expectedValue: true,
|
|
144
|
+
actualValue: false,
|
|
145
|
+
mutability: PropertyMutability.MUTABLE,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const issues = [
|
|
149
|
+
Issue.propertyMismatch({
|
|
150
|
+
resourceType: 'AWS::EC2::VPC',
|
|
151
|
+
resourceId: 'vpc-123',
|
|
152
|
+
mismatch: mismatch1,
|
|
153
|
+
}),
|
|
154
|
+
Issue.propertyMismatch({
|
|
155
|
+
resourceType: 'AWS::EC2::VPC',
|
|
156
|
+
resourceId: 'vpc-123',
|
|
157
|
+
mismatch: mismatch2,
|
|
158
|
+
}),
|
|
159
|
+
];
|
|
160
|
+
|
|
161
|
+
const score = calculator.calculate({ resources: [], issues });
|
|
162
|
+
expect(score.value).toBe(80); // 100 - 10 - 10
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe('info issues (minor penalties)', () => {
|
|
167
|
+
it('should deduct 5 points for missing tag', () => {
|
|
168
|
+
const issues = [
|
|
169
|
+
new Issue({
|
|
170
|
+
type: 'MISSING_TAG',
|
|
171
|
+
severity: 'info',
|
|
172
|
+
resourceType: 'AWS::Lambda::Function',
|
|
173
|
+
resourceId: 'my-function',
|
|
174
|
+
description: 'Missing Environment tag',
|
|
175
|
+
}),
|
|
176
|
+
];
|
|
177
|
+
|
|
178
|
+
const score = calculator.calculate({ resources: [], issues });
|
|
179
|
+
expect(score.value).toBe(95); // 100 - 5
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('should deduct 10 points for two missing tags', () => {
|
|
183
|
+
const issues = [
|
|
184
|
+
new Issue({
|
|
185
|
+
type: 'MISSING_TAG',
|
|
186
|
+
severity: 'info',
|
|
187
|
+
resourceType: 'AWS::Lambda::Function',
|
|
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
|
+
];
|
|
199
|
+
|
|
200
|
+
const score = calculator.calculate({ resources: [], issues });
|
|
201
|
+
expect(score.value).toBe(90); // 100 - 5 - 5
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
describe('mixed severity issues', () => {
|
|
206
|
+
it('should correctly combine penalties from different severity levels', () => {
|
|
207
|
+
const orphanedIssue = Issue.orphanedResource({
|
|
208
|
+
resourceType: 'AWS::RDS::DBCluster',
|
|
209
|
+
resourceId: 'orphan-123',
|
|
210
|
+
description: 'Orphaned cluster',
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const mismatch = new PropertyMismatch({
|
|
214
|
+
propertyPath: 'Properties.Tags',
|
|
215
|
+
expectedValue: ['tag1'],
|
|
216
|
+
actualValue: ['tag2'],
|
|
217
|
+
mutability: PropertyMutability.MUTABLE,
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const warningIssue = Issue.propertyMismatch({
|
|
221
|
+
resourceType: 'AWS::EC2::VPC',
|
|
222
|
+
resourceId: 'vpc-123',
|
|
223
|
+
mismatch,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const infoIssue = new Issue({
|
|
227
|
+
type: 'MISSING_TAG',
|
|
228
|
+
severity: 'info',
|
|
229
|
+
resourceType: 'AWS::Lambda::Function',
|
|
230
|
+
resourceId: 'my-function',
|
|
231
|
+
description: 'Missing tag',
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const issues = [orphanedIssue, warningIssue, infoIssue];
|
|
235
|
+
|
|
236
|
+
const score = calculator.calculate({ resources: [], issues });
|
|
237
|
+
expect(score.value).toBe(55); // 100 - 30 (critical) - 10 (warning) - 5 (info)
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
describe('minimum score (0)', () => {
|
|
242
|
+
it('should not go below 0', () => {
|
|
243
|
+
// Create enough critical issues to exceed 100 points
|
|
244
|
+
const issues = [
|
|
245
|
+
Issue.orphanedResource({
|
|
246
|
+
resourceType: 'AWS::RDS::DBCluster',
|
|
247
|
+
resourceId: 'orphan-1',
|
|
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({
|
|
256
|
+
resourceType: 'AWS::RDS::DBCluster',
|
|
257
|
+
resourceId: 'orphan-3',
|
|
258
|
+
description: 'Orphaned cluster 3',
|
|
259
|
+
}),
|
|
260
|
+
Issue.orphanedResource({
|
|
261
|
+
resourceType: 'AWS::RDS::DBCluster',
|
|
262
|
+
resourceId: 'orphan-4',
|
|
263
|
+
description: 'Orphaned cluster 4',
|
|
264
|
+
}),
|
|
265
|
+
];
|
|
266
|
+
|
|
267
|
+
const score = calculator.calculate({ resources: [], issues });
|
|
268
|
+
expect(score.value).toBe(0); // Should cap at 0, not go negative
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
describe('penalty configuration', () => {
|
|
273
|
+
it('should use custom penalty configuration', () => {
|
|
274
|
+
const customCalculator = new HealthScoreCalculator({
|
|
275
|
+
penalties: {
|
|
276
|
+
critical: 20, // Custom: 20 instead of default 30
|
|
277
|
+
warning: 5, // Custom: 5 instead of default 10
|
|
278
|
+
info: 2, // Custom: 2 instead of default 5
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
const issues = [
|
|
283
|
+
Issue.orphanedResource({
|
|
284
|
+
resourceType: 'AWS::RDS::DBCluster',
|
|
285
|
+
resourceId: 'orphan-123',
|
|
286
|
+
description: 'Orphaned cluster',
|
|
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();
|
|
324
|
+
|
|
325
|
+
expect(penalties).toEqual({
|
|
326
|
+
critical: 30,
|
|
327
|
+
warning: 10,
|
|
328
|
+
info: 5,
|
|
329
|
+
immutablePropertyMismatch: 20,
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
describe('explainScore', () => {
|
|
335
|
+
it('should explain score with breakdown', () => {
|
|
336
|
+
const orphanedIssue = Issue.orphanedResource({
|
|
337
|
+
resourceType: 'AWS::RDS::DBCluster',
|
|
338
|
+
resourceId: 'orphan-123',
|
|
339
|
+
description: 'Orphaned cluster',
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
const mismatch = new PropertyMismatch({
|
|
343
|
+
propertyPath: 'Properties.Tags',
|
|
344
|
+
expectedValue: ['tag1'],
|
|
345
|
+
actualValue: ['tag2'],
|
|
346
|
+
mutability: PropertyMutability.MUTABLE,
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
const warningIssue = Issue.propertyMismatch({
|
|
350
|
+
resourceType: 'AWS::EC2::VPC',
|
|
351
|
+
resourceId: 'vpc-123',
|
|
352
|
+
mismatch,
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
const infoIssue = new Issue({
|
|
356
|
+
type: 'MISSING_TAG',
|
|
357
|
+
severity: 'info',
|
|
358
|
+
resourceType: 'AWS::Lambda::Function',
|
|
359
|
+
resourceId: 'my-function',
|
|
360
|
+
description: 'Missing tag',
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
const issues = [orphanedIssue, warningIssue, infoIssue];
|
|
364
|
+
|
|
365
|
+
const explanation = calculator.explainScore({ resources: [], issues });
|
|
366
|
+
|
|
367
|
+
expect(explanation).toEqual({
|
|
368
|
+
finalScore: 55,
|
|
369
|
+
startingScore: 100,
|
|
370
|
+
totalPenalty: 45,
|
|
371
|
+
breakdown: {
|
|
372
|
+
critical: { count: 1, penalty: 30 },
|
|
373
|
+
warning: { count: 1, penalty: 10 },
|
|
374
|
+
info: { count: 1, penalty: 5 },
|
|
375
|
+
},
|
|
376
|
+
issueTypes: {
|
|
377
|
+
ORPHANED_RESOURCE: 1,
|
|
378
|
+
PROPERTY_MISMATCH: 1,
|
|
379
|
+
MISSING_TAG: 1,
|
|
380
|
+
},
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it('should explain perfect score', () => {
|
|
385
|
+
const explanation = calculator.explainScore({ resources: [], issues: [] });
|
|
386
|
+
|
|
387
|
+
expect(explanation).toEqual({
|
|
388
|
+
finalScore: 100,
|
|
389
|
+
startingScore: 100,
|
|
390
|
+
totalPenalty: 0,
|
|
391
|
+
breakdown: {
|
|
392
|
+
critical: { count: 0, penalty: 0 },
|
|
393
|
+
warning: { count: 0, penalty: 0 },
|
|
394
|
+
info: { count: 0, penalty: 0 },
|
|
395
|
+
},
|
|
396
|
+
issueTypes: {},
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
});
|