@maverick006/security-engine 1.0.2 → 1.0.4
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/dist/deduplication.d.ts +16 -0
- package/dist/deduplication.d.ts.map +1 -0
- package/dist/deduplication.js +101 -0
- package/dist/deduplication.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/orchestrator.d.ts +13 -8
- package/dist/orchestrator.d.ts.map +1 -1
- package/dist/orchestrator.js +153 -38
- package/dist/orchestrator.js.map +1 -1
- package/dist/scanner.d.ts +9 -0
- package/dist/scanner.d.ts.map +1 -1
- package/dist/scoring.d.ts +19 -13
- package/dist/scoring.d.ts.map +1 -1
- package/dist/scoring.js +96 -26
- package/dist/scoring.js.map +1 -1
- package/package.json +5 -4
- package/src/deduplication.ts +118 -0
- package/src/index.ts +1 -0
- package/src/orchestrator.ts +183 -46
- package/src/scanner.ts +11 -0
- package/src/scoring.ts +107 -30
- package/tests/deduplication.test.ts +122 -0
- package/tests/orchestrator.test.ts +120 -96
- package/tests/scoring.test.ts +33 -25
- package/tsconfig.tsbuildinfo +0 -1
package/src/scoring.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import { NormalizedFinding, Severity } from '@maverick006/types';
|
|
1
|
+
import { NormalizedFinding, Severity, ScannerCoverage, DeterministicScore, ScoreDeductions } from '@maverick006/types';
|
|
2
2
|
|
|
3
|
-
export interface ScoreResult {
|
|
4
|
-
score: number;
|
|
5
|
-
grade: 'A' | 'B' | 'C' | 'D' | 'F';
|
|
3
|
+
export interface ScoreResult extends DeterministicScore {
|
|
6
4
|
metrics: {
|
|
7
5
|
critical: number;
|
|
8
6
|
high: number;
|
|
@@ -12,68 +10,147 @@ export interface ScoreResult {
|
|
|
12
10
|
}
|
|
13
11
|
|
|
14
12
|
/**
|
|
15
|
-
* Calculates a deterministic security score
|
|
16
|
-
*
|
|
13
|
+
* Calculates a deterministic, reproducible security score (0 to 100) and grade
|
|
14
|
+
* based on the volume and severity of unique findings.
|
|
17
15
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
16
|
+
* Rules:
|
|
17
|
+
* - Base score: 100
|
|
18
|
+
* - Critical: -30 points per finding (any critical forces Grade F and caps score at <= 49)
|
|
19
|
+
* - High: -10 points per finding
|
|
20
|
+
* - Medium: -3 points per finding
|
|
21
|
+
* - Low: -1 point per finding
|
|
22
|
+
* - Info: 0 points
|
|
23
|
+
*
|
|
24
|
+
* Grades:
|
|
25
|
+
* A: 90 - 100
|
|
26
|
+
* B: 80 - 89
|
|
27
|
+
* C: 70 - 79
|
|
28
|
+
* D: 50 - 69
|
|
29
|
+
* F: < 50 OR any CRITICAL finding
|
|
24
30
|
*/
|
|
25
|
-
export function calculateScore(
|
|
31
|
+
export function calculateScore(
|
|
32
|
+
findings: NormalizedFinding[],
|
|
33
|
+
coverage?: Partial<ScannerCoverage>
|
|
34
|
+
): ScoreResult {
|
|
26
35
|
let critical = 0;
|
|
27
36
|
let high = 0;
|
|
28
37
|
let medium = 0;
|
|
29
38
|
let low = 0;
|
|
30
|
-
let
|
|
39
|
+
let info = 0;
|
|
31
40
|
|
|
32
41
|
for (const finding of findings) {
|
|
33
|
-
|
|
42
|
+
const sev = (finding.severity || '').toUpperCase();
|
|
43
|
+
switch (sev) {
|
|
34
44
|
case Severity.CRITICAL:
|
|
45
|
+
case 'CRITICAL':
|
|
35
46
|
critical++;
|
|
36
|
-
score += 100;
|
|
37
47
|
break;
|
|
38
48
|
case Severity.HIGH:
|
|
49
|
+
case 'HIGH':
|
|
39
50
|
high++;
|
|
40
|
-
score += 20;
|
|
41
51
|
break;
|
|
42
52
|
case Severity.MEDIUM:
|
|
53
|
+
case 'MEDIUM':
|
|
43
54
|
medium++;
|
|
44
|
-
score += 5;
|
|
45
55
|
break;
|
|
46
56
|
case Severity.LOW:
|
|
47
|
-
case
|
|
57
|
+
case 'LOW':
|
|
48
58
|
low++;
|
|
49
|
-
|
|
59
|
+
break;
|
|
60
|
+
default:
|
|
61
|
+
info++;
|
|
50
62
|
break;
|
|
51
63
|
}
|
|
52
64
|
}
|
|
53
65
|
|
|
66
|
+
const critDeduction = critical * 30;
|
|
67
|
+
const highDeduction = high * 10;
|
|
68
|
+
const medDeduction = medium * 3;
|
|
69
|
+
const lowDeduction = low * 1;
|
|
70
|
+
const totalDeductions = critDeduction + highDeduction + medDeduction + lowDeduction;
|
|
71
|
+
|
|
72
|
+
let rawScore = 100 - totalDeductions;
|
|
73
|
+
let score = Math.max(0, Math.min(100, rawScore));
|
|
74
|
+
|
|
75
|
+
const explanation: string[] = [
|
|
76
|
+
'Baseline score: 100/100'
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
if (critical > 0) {
|
|
80
|
+
explanation.push(`-${critDeduction} points: ${critical} Critical severity ${critical === 1 ? 'finding' : 'findings'} (-30 pts each)`);
|
|
81
|
+
}
|
|
82
|
+
if (high > 0) {
|
|
83
|
+
explanation.push(`-${highDeduction} points: ${high} High severity ${high === 1 ? 'finding' : 'findings'} (-10 pts each)`);
|
|
84
|
+
}
|
|
85
|
+
if (medium > 0) {
|
|
86
|
+
explanation.push(`-${medDeduction} points: ${medium} Medium severity ${medium === 1 ? 'finding' : 'findings'} (-3 pts each)`);
|
|
87
|
+
}
|
|
88
|
+
if (low > 0) {
|
|
89
|
+
explanation.push(`-${lowDeduction} points: ${low} Low severity ${low === 1 ? 'finding' : 'findings'} (-1 pt each)`);
|
|
90
|
+
}
|
|
91
|
+
if (findings.length === 0) {
|
|
92
|
+
explanation.push('No security findings identified across executed scanners (+0 deductions)');
|
|
93
|
+
}
|
|
94
|
+
|
|
54
95
|
let grade: ScoreResult['grade'] = 'A';
|
|
55
96
|
|
|
56
97
|
if (critical > 0) {
|
|
57
|
-
//
|
|
98
|
+
// Critical vulnerability automatically caps grade to F and score to at most 49
|
|
58
99
|
grade = 'F';
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
} else if (score
|
|
62
|
-
grade = '
|
|
63
|
-
} else if (score
|
|
64
|
-
grade = 'C';
|
|
65
|
-
} else if (score > 10) {
|
|
100
|
+
score = Math.min(score, 49);
|
|
101
|
+
explanation.push(`Grade Override: F (1 or more Critical severity findings detected)`);
|
|
102
|
+
} else if (score >= 90) {
|
|
103
|
+
grade = 'A';
|
|
104
|
+
} else if (score >= 80) {
|
|
66
105
|
grade = 'B';
|
|
106
|
+
} else if (score >= 70) {
|
|
107
|
+
grade = 'C';
|
|
108
|
+
} else if (score >= 50) {
|
|
109
|
+
grade = 'D';
|
|
110
|
+
} else {
|
|
111
|
+
grade = 'F';
|
|
67
112
|
}
|
|
68
113
|
|
|
114
|
+
explanation.push(`Final deterministic score: ${score}/100 (Grade ${grade})`);
|
|
115
|
+
|
|
116
|
+
const deductions: ScoreDeductions = {
|
|
117
|
+
critical: critDeduction,
|
|
118
|
+
high: highDeduction,
|
|
119
|
+
medium: medDeduction,
|
|
120
|
+
low: lowDeduction,
|
|
121
|
+
info: 0,
|
|
122
|
+
totalDeductions
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const defaultCoverage: ScannerCoverage = {
|
|
126
|
+
code: false,
|
|
127
|
+
dependencies: false,
|
|
128
|
+
secrets: false,
|
|
129
|
+
containers: false,
|
|
130
|
+
iac: false,
|
|
131
|
+
web: false,
|
|
132
|
+
cloud: false,
|
|
133
|
+
...coverage
|
|
134
|
+
};
|
|
135
|
+
|
|
69
136
|
return {
|
|
70
137
|
score,
|
|
71
138
|
grade,
|
|
72
|
-
|
|
139
|
+
deductions,
|
|
140
|
+
breakdown: {
|
|
73
141
|
critical,
|
|
74
142
|
high,
|
|
75
143
|
medium,
|
|
76
144
|
low,
|
|
77
|
-
|
|
145
|
+
info
|
|
146
|
+
},
|
|
147
|
+
metrics: {
|
|
148
|
+
critical,
|
|
149
|
+
high,
|
|
150
|
+
medium,
|
|
151
|
+
low
|
|
152
|
+
},
|
|
153
|
+
coverage: defaultCoverage,
|
|
154
|
+
explanation
|
|
78
155
|
};
|
|
79
156
|
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { deduplicateFindings } from '../src/deduplication';
|
|
2
|
+
import { NormalizedFinding, Severity } from '@maverick006/types';
|
|
3
|
+
|
|
4
|
+
describe('Cross-Scanner Deduplication Engine', () => {
|
|
5
|
+
it('should remove exact duplicate findings from the same scanner', () => {
|
|
6
|
+
const findings: NormalizedFinding[] = [
|
|
7
|
+
{
|
|
8
|
+
scanner: 'Semgrep',
|
|
9
|
+
ruleId: 'sql-injection',
|
|
10
|
+
title: 'SQL Injection in Query',
|
|
11
|
+
description: 'User input concatenation',
|
|
12
|
+
severity: Severity.CRITICAL,
|
|
13
|
+
file: 'src/db.ts',
|
|
14
|
+
line: 42,
|
|
15
|
+
column: 10
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
scanner: 'Semgrep',
|
|
19
|
+
ruleId: 'sql-injection',
|
|
20
|
+
title: 'SQL Injection in Query',
|
|
21
|
+
description: 'User input concatenation',
|
|
22
|
+
severity: Severity.CRITICAL,
|
|
23
|
+
file: 'src/db.ts',
|
|
24
|
+
line: 42,
|
|
25
|
+
column: 10
|
|
26
|
+
}
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const result = deduplicateFindings(findings);
|
|
30
|
+
expect(result).toHaveLength(1);
|
|
31
|
+
expect(result[0].scanner).toBe('Semgrep');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should deduplicate cross-scanner findings with same CVE on same file/package', () => {
|
|
35
|
+
const findings: NormalizedFinding[] = [
|
|
36
|
+
{
|
|
37
|
+
scanner: 'npm-audit',
|
|
38
|
+
ruleId: 'CVE-2023-45133',
|
|
39
|
+
title: 'CVE-2023-45133 in @babel/traverse',
|
|
40
|
+
description: 'Arbitrary code execution via traverse',
|
|
41
|
+
severity: Severity.HIGH,
|
|
42
|
+
package: '@babel/traverse',
|
|
43
|
+
packageVersion: '7.23.0',
|
|
44
|
+
fixedVersion: '7.23.2',
|
|
45
|
+
file: 'package-lock.json'
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
scanner: 'Trivy',
|
|
49
|
+
ruleId: 'CVE-2023-45133',
|
|
50
|
+
title: 'Babel traverse code execution (CVE-2023-45133)',
|
|
51
|
+
description: 'Arbitrary code execution in babel/traverse',
|
|
52
|
+
severity: Severity.HIGH,
|
|
53
|
+
package: '@babel/traverse',
|
|
54
|
+
packageVersion: '7.23.0',
|
|
55
|
+
fixedVersion: '7.23.2',
|
|
56
|
+
file: 'package-lock.json'
|
|
57
|
+
}
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
const result = deduplicateFindings(findings);
|
|
61
|
+
expect(result).toHaveLength(1);
|
|
62
|
+
expect(result[0].scanner).toContain('npm-audit');
|
|
63
|
+
expect(result[0].scanner).toContain('Trivy');
|
|
64
|
+
expect(result[0].package).toBe('@babel/traverse');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should deduplicate findings with nearby line shifts (+/- 3 lines)', () => {
|
|
68
|
+
const findings: NormalizedFinding[] = [
|
|
69
|
+
{
|
|
70
|
+
scanner: 'Semgrep',
|
|
71
|
+
ruleId: 'hardcoded-secret',
|
|
72
|
+
title: 'Hardcoded API Token',
|
|
73
|
+
description: 'Detected high entropy string',
|
|
74
|
+
severity: Severity.HIGH,
|
|
75
|
+
file: 'src/config.ts',
|
|
76
|
+
line: 15
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
scanner: 'Gitleaks',
|
|
80
|
+
ruleId: 'hardcoded-secret',
|
|
81
|
+
title: 'Generic API Key',
|
|
82
|
+
description: 'Detected secret key',
|
|
83
|
+
severity: Severity.HIGH,
|
|
84
|
+
file: 'src/config.ts',
|
|
85
|
+
line: 17 // within 2 lines
|
|
86
|
+
}
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
const result = deduplicateFindings(findings, { lineTolerance: 3 });
|
|
90
|
+
expect(result).toHaveLength(1);
|
|
91
|
+
expect(result[0].scanner).toContain('Semgrep');
|
|
92
|
+
expect(result[0].scanner).toContain('Gitleaks');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should preserve distinct vulnerabilities on the same file and line', () => {
|
|
96
|
+
const findings: NormalizedFinding[] = [
|
|
97
|
+
{
|
|
98
|
+
scanner: 'Semgrep',
|
|
99
|
+
ruleId: 'sqli-rule-1',
|
|
100
|
+
title: 'SQL Injection',
|
|
101
|
+
description: 'Unescaped SQL query',
|
|
102
|
+
severity: Severity.CRITICAL,
|
|
103
|
+
file: 'src/api/user.ts',
|
|
104
|
+
line: 25
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
scanner: 'Semgrep',
|
|
108
|
+
ruleId: 'xss-rule-2',
|
|
109
|
+
title: 'Cross-Site Scripting (XSS)',
|
|
110
|
+
description: 'Unsanitized HTML rendering',
|
|
111
|
+
severity: Severity.HIGH,
|
|
112
|
+
file: 'src/api/user.ts',
|
|
113
|
+
line: 25
|
|
114
|
+
}
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
const result = deduplicateFindings(findings);
|
|
118
|
+
expect(result).toHaveLength(2);
|
|
119
|
+
expect(result.map(r => r.title)).toContain('SQL Injection');
|
|
120
|
+
expect(result.map(r => r.title)).toContain('Cross-Site Scripting (XSS)');
|
|
121
|
+
});
|
|
122
|
+
});
|
|
@@ -1,96 +1,120 @@
|
|
|
1
|
-
import { Orchestrator } from '../src/orchestrator';
|
|
2
|
-
import { SecurityScanner } from '../src/scanner';
|
|
3
|
-
import { ScanInput, ScannerResult,
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const result = await orchestrator.runScan(
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
expect(result.
|
|
59
|
-
expect(result.
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('should
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
expect(
|
|
95
|
-
|
|
96
|
-
|
|
1
|
+
import { Orchestrator } from '../src/orchestrator';
|
|
2
|
+
import { SecurityScanner } from '../src/scanner';
|
|
3
|
+
import { ScanInput, ScannerResult, ScannerState, Severity } from '@maverick006/types';
|
|
4
|
+
|
|
5
|
+
describe('Security Orchestrator with Bounded Concurrency', () => {
|
|
6
|
+
const mockInput: ScanInput = {
|
|
7
|
+
scanId: 'test-scan-123',
|
|
8
|
+
repositoryPath: '/mock/repo'
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
it('should execute scanners with bounded concurrency and aggregate findings', async () => {
|
|
12
|
+
let runningCount = 0;
|
|
13
|
+
let maxConcurrent = 0;
|
|
14
|
+
|
|
15
|
+
const createMockScanner = (name: string, delayMs: number): SecurityScanner => ({
|
|
16
|
+
name,
|
|
17
|
+
scan: async (input: ScanInput): Promise<ScannerResult> => {
|
|
18
|
+
runningCount++;
|
|
19
|
+
maxConcurrent = Math.max(maxConcurrent, runningCount);
|
|
20
|
+
await new Promise(resolve => setTimeout(resolve, delayMs));
|
|
21
|
+
runningCount--;
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
scanner: name,
|
|
25
|
+
success: true,
|
|
26
|
+
state: ScannerState.SUCCESS,
|
|
27
|
+
findings: [
|
|
28
|
+
{
|
|
29
|
+
scanner: name,
|
|
30
|
+
ruleId: `${name}-rule`,
|
|
31
|
+
title: `${name} finding`,
|
|
32
|
+
description: 'sample',
|
|
33
|
+
severity: Severity.LOW,
|
|
34
|
+
file: `${name}.ts`,
|
|
35
|
+
line: 1
|
|
36
|
+
}
|
|
37
|
+
],
|
|
38
|
+
startTime: new Date(),
|
|
39
|
+
endTime: new Date()
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const scanners: SecurityScanner[] = [
|
|
45
|
+
createMockScanner('Semgrep', 30),
|
|
46
|
+
createMockScanner('Gitleaks', 30),
|
|
47
|
+
createMockScanner('Trivy', 30),
|
|
48
|
+
createMockScanner('npm-audit', 30),
|
|
49
|
+
createMockScanner('Checkov', 30)
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
const orchestrator = new Orchestrator(scanners, { concurrencyLimit: 2 });
|
|
53
|
+
const result = await orchestrator.runScan(mockInput);
|
|
54
|
+
|
|
55
|
+
expect(maxConcurrent).toBeLessThanOrEqual(2);
|
|
56
|
+
expect(result.scannerResults).toHaveLength(5);
|
|
57
|
+
expect(result.findings).toHaveLength(5);
|
|
58
|
+
expect(result.coverage.code).toBe(true);
|
|
59
|
+
expect(result.coverage.secrets).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should handle timeout gracefully without crashing the whole scan', async () => {
|
|
63
|
+
const normalScanner: SecurityScanner = {
|
|
64
|
+
name: 'Semgrep',
|
|
65
|
+
scan: async () => ({
|
|
66
|
+
scanner: 'Semgrep',
|
|
67
|
+
success: true,
|
|
68
|
+
state: ScannerState.SUCCESS,
|
|
69
|
+
findings: [],
|
|
70
|
+
startTime: new Date(),
|
|
71
|
+
endTime: new Date()
|
|
72
|
+
})
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const slowScanner: SecurityScanner = {
|
|
76
|
+
name: 'SlowScanner',
|
|
77
|
+
scan: async () => {
|
|
78
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
79
|
+
return {
|
|
80
|
+
scanner: 'SlowScanner',
|
|
81
|
+
success: true,
|
|
82
|
+
findings: [],
|
|
83
|
+
startTime: new Date(),
|
|
84
|
+
endTime: new Date()
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const orchestrator = new Orchestrator([normalScanner, slowScanner], { timeoutMs: 50 });
|
|
90
|
+
const result = await orchestrator.runScan(mockInput);
|
|
91
|
+
|
|
92
|
+
expect(result.scannerResults).toHaveLength(2);
|
|
93
|
+
const slowResult = result.scannerResults.find(r => r.scanner === 'SlowScanner');
|
|
94
|
+
expect(slowResult?.state).toBe(ScannerState.TIMEOUT);
|
|
95
|
+
expect(slowResult?.success).toBe(false);
|
|
96
|
+
|
|
97
|
+
const normalResult = result.scannerResults.find(r => r.scanner === 'Semgrep');
|
|
98
|
+
expect(normalResult?.state).toBe(ScannerState.SUCCESS);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should detect when scanner capability detects repo is not applicable and return SKIPPED', async () => {
|
|
102
|
+
const iacScanner: SecurityScanner = {
|
|
103
|
+
name: 'Checkov',
|
|
104
|
+
capabilities: {
|
|
105
|
+
category: 'iac',
|
|
106
|
+
detectApplicability: async () => false // No IaC files
|
|
107
|
+
},
|
|
108
|
+
scan: async () => {
|
|
109
|
+
throw new Error('Should not be called when detectApplicability returns false');
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const orchestrator = new Orchestrator([iacScanner]);
|
|
114
|
+
const result = await orchestrator.runScan(mockInput);
|
|
115
|
+
|
|
116
|
+
expect(result.scannerResults).toHaveLength(1);
|
|
117
|
+
expect(result.scannerResults[0].state).toBe(ScannerState.SKIPPED);
|
|
118
|
+
expect(result.scannerResults[0].findings).toHaveLength(0);
|
|
119
|
+
});
|
|
120
|
+
});
|
package/tests/scoring.test.ts
CHANGED
|
@@ -1,62 +1,70 @@
|
|
|
1
1
|
import { calculateScore } from '../src/scoring';
|
|
2
2
|
import { NormalizedFinding, Severity } from '@maverick006/types';
|
|
3
3
|
|
|
4
|
-
describe('Deterministic Scoring Engine', () => {
|
|
5
|
-
|
|
6
|
-
const createFinding = (severity: Severity): NormalizedFinding => ({
|
|
4
|
+
describe('Deterministic Scoring Engine (0-100)', () => {
|
|
5
|
+
const createFinding = (severity: Severity, title = 'test finding'): NormalizedFinding => ({
|
|
7
6
|
scanner: 'test',
|
|
8
7
|
ruleId: 'test-rule',
|
|
9
|
-
title
|
|
8
|
+
title,
|
|
10
9
|
description: 'test desc',
|
|
11
10
|
severity,
|
|
12
11
|
file: 'test.ts',
|
|
13
12
|
line: 1
|
|
14
13
|
});
|
|
15
14
|
|
|
16
|
-
it('should return grade A for perfect score (0 findings)', () => {
|
|
15
|
+
it('should return score 100 and grade A for perfect score (0 findings)', () => {
|
|
17
16
|
const result = calculateScore([]);
|
|
18
|
-
expect(result.score).toBe(
|
|
17
|
+
expect(result.score).toBe(100);
|
|
19
18
|
expect(result.grade).toBe('A');
|
|
19
|
+
expect(result.deductions.totalDeductions).toBe(0);
|
|
20
|
+
expect(result.explanation).toContain('Baseline score: 100/100');
|
|
21
|
+
expect(result.explanation.some(e => e.includes('No security findings identified'))).toBe(true);
|
|
20
22
|
});
|
|
21
23
|
|
|
22
|
-
it('should
|
|
24
|
+
it('should deduct 3 points per medium finding (e.g. 2 mediums = 94, Grade A)', () => {
|
|
23
25
|
const findings = [
|
|
24
|
-
createFinding(Severity.MEDIUM), //
|
|
25
|
-
createFinding(Severity.MEDIUM), //
|
|
26
|
+
createFinding(Severity.MEDIUM), // -3
|
|
27
|
+
createFinding(Severity.MEDIUM), // -3
|
|
26
28
|
];
|
|
27
29
|
const result = calculateScore(findings);
|
|
28
|
-
expect(result.score).toBe(
|
|
30
|
+
expect(result.score).toBe(94);
|
|
29
31
|
expect(result.grade).toBe('A');
|
|
32
|
+
expect(result.deductions.medium).toBe(6);
|
|
33
|
+
expect(result.breakdown.medium).toBe(2);
|
|
30
34
|
});
|
|
31
35
|
|
|
32
|
-
it('should
|
|
36
|
+
it('should deduct 10 for high and 1 for low (e.g. 1 high, 2 lows = 88, Grade B)', () => {
|
|
33
37
|
const findings = [
|
|
34
|
-
createFinding(Severity.HIGH), //
|
|
35
|
-
createFinding(Severity.LOW), // 1
|
|
36
|
-
createFinding(Severity.LOW), // 1
|
|
38
|
+
createFinding(Severity.HIGH), // -10
|
|
39
|
+
createFinding(Severity.LOW), // -1
|
|
40
|
+
createFinding(Severity.LOW), // -1
|
|
37
41
|
];
|
|
38
42
|
const result = calculateScore(findings);
|
|
39
|
-
expect(result.score).toBe(
|
|
43
|
+
expect(result.score).toBe(88);
|
|
40
44
|
expect(result.grade).toBe('B');
|
|
45
|
+
expect(result.deductions.high).toBe(10);
|
|
46
|
+
expect(result.deductions.low).toBe(2);
|
|
47
|
+
expect(result.deductions.totalDeductions).toBe(12);
|
|
41
48
|
});
|
|
42
49
|
|
|
43
|
-
it('should immediately
|
|
50
|
+
it('should immediately force grade F and cap score to <= 49 if any CRITICAL exists', () => {
|
|
44
51
|
const findings = [
|
|
45
|
-
createFinding(Severity.CRITICAL), //
|
|
46
|
-
createFinding(Severity.LOW), // 1
|
|
52
|
+
createFinding(Severity.CRITICAL), // -30, and caps grade to F, score to <= 49
|
|
53
|
+
createFinding(Severity.LOW), // -1
|
|
47
54
|
];
|
|
48
55
|
const result = calculateScore(findings);
|
|
49
|
-
|
|
50
|
-
expect(result.score).toBe(101);
|
|
56
|
+
expect(result.score).toBeLessThanOrEqual(49);
|
|
51
57
|
expect(result.grade).toBe('F');
|
|
58
|
+
expect(result.deductions.critical).toBe(30);
|
|
59
|
+
expect(result.breakdown.critical).toBe(1);
|
|
60
|
+
expect(result.explanation.some(e => e.includes('Grade Override: F'))).toBe(true);
|
|
52
61
|
});
|
|
53
62
|
|
|
54
|
-
it('should return grade F
|
|
55
|
-
//
|
|
56
|
-
const findings = Array(8).fill(createFinding(Severity.HIGH));
|
|
63
|
+
it('should clamp minimum score at 0 and return grade F for heavy deductions', () => {
|
|
64
|
+
const findings = Array(15).fill(createFinding(Severity.HIGH)); // 15 * 10 = -150
|
|
57
65
|
const result = calculateScore(findings);
|
|
58
|
-
expect(result.score).toBe(
|
|
66
|
+
expect(result.score).toBe(0);
|
|
59
67
|
expect(result.grade).toBe('F');
|
|
68
|
+
expect(result.deductions.totalDeductions).toBe(150);
|
|
60
69
|
});
|
|
61
|
-
|
|
62
70
|
});
|