@maverick006/vibeguard 1.0.10 → 1.0.12
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/credentials.js +63 -0
- package/dist/formatter.js +365 -121
- package/dist/index.js +317 -107
- package/jest.config.js +5 -0
- package/package.json +7 -3
- package/src/credentials.ts +74 -0
- package/src/formatter.ts +446 -143
- package/src/index.ts +455 -211
- package/tests/auth.test.ts +96 -0
- package/tests/formatter.test.ts +666 -0
|
@@ -0,0 +1,666 @@
|
|
|
1
|
+
import {
|
|
2
|
+
renderDashboard,
|
|
3
|
+
calculateScore,
|
|
4
|
+
maskSecrets,
|
|
5
|
+
renderCIOutput,
|
|
6
|
+
generateJsonOutput,
|
|
7
|
+
evaluatePolicy,
|
|
8
|
+
ScanStats,
|
|
9
|
+
AIRemediationData,
|
|
10
|
+
ScannerTelemetry
|
|
11
|
+
} from '../src/formatter';
|
|
12
|
+
import { NormalizedFinding, Severity, ScannerCoverage, DeterministicScore } from '@maverick006/types';
|
|
13
|
+
|
|
14
|
+
describe('VibeGuard CLI UX & Formatter Test Suite', () => {
|
|
15
|
+
function captureOutput(fn: () => void): string {
|
|
16
|
+
const logs: string[] = [];
|
|
17
|
+
const spy = jest.spyOn(console, 'log').mockImplementation((...args) => {
|
|
18
|
+
logs.push(args.join(' '));
|
|
19
|
+
});
|
|
20
|
+
try {
|
|
21
|
+
fn();
|
|
22
|
+
} finally {
|
|
23
|
+
spy.mockRestore();
|
|
24
|
+
}
|
|
25
|
+
return logs.join('\n').replace(/\u001b\[[0-9;]*m/g, '');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const dummyGitInfo = {
|
|
29
|
+
name: 'VibeGuard',
|
|
30
|
+
branch: 'main',
|
|
31
|
+
commit: 'a1b2c3d',
|
|
32
|
+
remoteUrl: 'https://github.com/Maverickrd007/VibeGuard.git'
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const perfectCoverage: ScannerCoverage = {
|
|
36
|
+
code: true,
|
|
37
|
+
dependencies: true,
|
|
38
|
+
secrets: true,
|
|
39
|
+
containers: true,
|
|
40
|
+
iac: true,
|
|
41
|
+
web: true,
|
|
42
|
+
cloud: true
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const createFinding = (overrides: Partial<NormalizedFinding> = {}): NormalizedFinding => ({
|
|
46
|
+
id: 'VG-FIND-001',
|
|
47
|
+
scanner: 'test-scanner',
|
|
48
|
+
ruleId: 'test-rule',
|
|
49
|
+
title: 'Test Finding',
|
|
50
|
+
description: 'Test vulnerability description',
|
|
51
|
+
severity: Severity.LOW,
|
|
52
|
+
file: 'src/index.ts',
|
|
53
|
+
line: 1,
|
|
54
|
+
...overrides
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const dummyDeterministicScore = (
|
|
58
|
+
score: number,
|
|
59
|
+
grade: 'A' | 'B' | 'C' | 'D' | 'F',
|
|
60
|
+
overrides: Partial<DeterministicScore> = {}
|
|
61
|
+
): DeterministicScore => ({
|
|
62
|
+
score,
|
|
63
|
+
grade,
|
|
64
|
+
deductions: { critical: 0, high: 0, medium: 0, low: 0, info: 0, totalDeductions: 100 - score },
|
|
65
|
+
breakdown: { critical: 0, high: 0, medium: 0, low: 0, info: 0 },
|
|
66
|
+
coverage: perfectCoverage,
|
|
67
|
+
explanation: ['Baseline score: 100/100'],
|
|
68
|
+
...overrides
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// 1. Full scanner availability
|
|
72
|
+
it('1. Full scanner availability renders full score & full coverage', () => {
|
|
73
|
+
const scanners: ScannerTelemetry[] = [
|
|
74
|
+
{ scanner: 'Semgrep', state: 'SUCCESS', durationMs: 1500, findingsCount: 0 },
|
|
75
|
+
{ scanner: 'npm-audit', state: 'SUCCESS', durationMs: 800, findingsCount: 0 },
|
|
76
|
+
{ scanner: 'Gitleaks', state: 'SUCCESS', durationMs: 400, findingsCount: 0 },
|
|
77
|
+
{ scanner: 'Trivy', state: 'SUCCESS', durationMs: 1200, findingsCount: 0 },
|
|
78
|
+
{ scanner: 'Checkov', state: 'SUCCESS', durationMs: 1800, findingsCount: 0 },
|
|
79
|
+
{ scanner: 'OWASP ZAP', state: 'SUCCESS', durationMs: 2500, findingsCount: 0 },
|
|
80
|
+
{ scanner: 'Prowler', state: 'SUCCESS', durationMs: 3100, findingsCount: 0 }
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
const stats = calculateScore([]);
|
|
84
|
+
const output = captureOutput(() => {
|
|
85
|
+
renderDashboard({
|
|
86
|
+
findings: [],
|
|
87
|
+
stats,
|
|
88
|
+
deterministicScore: dummyDeterministicScore(100, 'A'),
|
|
89
|
+
coverage: perfectCoverage,
|
|
90
|
+
gitInfo: dummyGitInfo,
|
|
91
|
+
duration: '11.3s',
|
|
92
|
+
scanners
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
expect(output).toContain('COMPLETE POSTURE (7 / 7 security domains assessed)');
|
|
97
|
+
expect(output).toContain('Coverage: 7 / 7 security domains');
|
|
98
|
+
expect(output).toContain('100 / 100');
|
|
99
|
+
expect(output).toContain('Grade A');
|
|
100
|
+
expect(output).toContain('✓ Dependencies ✓ Code ✓ Secrets ✓ Containers ✓ IaC ✓ Web/API ✓ Cloud');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// 2. Partial scanner availability & PARTIAL POSTURE
|
|
104
|
+
it('2. Partial scanner availability renders PARTIAL POSTURE and correct domain count', () => {
|
|
105
|
+
const partialCoverage: ScannerCoverage = {
|
|
106
|
+
code: false,
|
|
107
|
+
dependencies: true,
|
|
108
|
+
secrets: false,
|
|
109
|
+
containers: false,
|
|
110
|
+
iac: false,
|
|
111
|
+
web: false,
|
|
112
|
+
cloud: false
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const scanners: ScannerTelemetry[] = [
|
|
116
|
+
{ scanner: 'npm-audit', state: 'SUCCESS', durationMs: 750, findingsCount: 1 },
|
|
117
|
+
{ scanner: 'Semgrep', state: 'NOT_INSTALLED' },
|
|
118
|
+
{ scanner: 'Gitleaks', state: 'NOT_INSTALLED' },
|
|
119
|
+
{ scanner: 'Trivy', state: 'NOT_INSTALLED' },
|
|
120
|
+
{ scanner: 'Checkov', state: 'NOT_INSTALLED' },
|
|
121
|
+
{ scanner: 'OWASP ZAP', state: 'SKIPPED', reason: 'No live web URL provided' },
|
|
122
|
+
{ scanner: 'Prowler', state: 'SKIPPED', reason: 'AWS credentials not configured' }
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
const finding = createFinding({
|
|
126
|
+
id: 'VG-FIND-001',
|
|
127
|
+
scanner: 'npm-audit',
|
|
128
|
+
ruleId: 'GHSA-4497',
|
|
129
|
+
title: 'Vulnerable package',
|
|
130
|
+
severity: Severity.LOW,
|
|
131
|
+
file: 'package.json',
|
|
132
|
+
line: 12
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const stats = calculateScore([finding]);
|
|
136
|
+
const output = captureOutput(() => {
|
|
137
|
+
renderDashboard({
|
|
138
|
+
findings: [finding],
|
|
139
|
+
stats,
|
|
140
|
+
deterministicScore: dummyDeterministicScore(99, 'A'),
|
|
141
|
+
coverage: partialCoverage,
|
|
142
|
+
gitInfo: dummyGitInfo,
|
|
143
|
+
duration: '0.8s',
|
|
144
|
+
scanners
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
expect(output).toContain('PARTIAL POSTURE (1 / 7 security domains assessed)');
|
|
149
|
+
expect(output).toContain('Coverage: 1 / 7 security domains');
|
|
150
|
+
expect(output).toContain('○ NOT INSTALLED');
|
|
151
|
+
expect(output).toContain('— NOT APPLICABLE');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// 3. All scanners unavailable
|
|
155
|
+
it('3. All scanners unavailable renders clean explanation and 0/7 domains', () => {
|
|
156
|
+
const zeroCoverage: ScannerCoverage = {
|
|
157
|
+
code: false,
|
|
158
|
+
dependencies: false,
|
|
159
|
+
secrets: false,
|
|
160
|
+
containers: false,
|
|
161
|
+
iac: false,
|
|
162
|
+
web: false,
|
|
163
|
+
cloud: false
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const scanners: ScannerTelemetry[] = [
|
|
167
|
+
{ scanner: 'Semgrep', state: 'NOT_INSTALLED' },
|
|
168
|
+
{ scanner: 'npm-audit', state: 'NOT_INSTALLED' },
|
|
169
|
+
{ scanner: 'Gitleaks', state: 'NOT_INSTALLED' },
|
|
170
|
+
{ scanner: 'Trivy', state: 'NOT_INSTALLED' },
|
|
171
|
+
{ scanner: 'Checkov', state: 'NOT_INSTALLED' },
|
|
172
|
+
{ scanner: 'OWASP ZAP', state: 'NOT_INSTALLED' },
|
|
173
|
+
{ scanner: 'Prowler', state: 'NOT_INSTALLED' }
|
|
174
|
+
];
|
|
175
|
+
|
|
176
|
+
const stats = calculateScore([]);
|
|
177
|
+
const output = captureOutput(() => {
|
|
178
|
+
renderDashboard({
|
|
179
|
+
findings: [],
|
|
180
|
+
stats,
|
|
181
|
+
deterministicScore: dummyDeterministicScore(100, 'A'),
|
|
182
|
+
coverage: zeroCoverage,
|
|
183
|
+
gitInfo: dummyGitInfo,
|
|
184
|
+
duration: '0.2s',
|
|
185
|
+
scanners
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
expect(output).toContain('PARTIAL POSTURE (0 / 7 security domains assessed)');
|
|
190
|
+
expect(output).toContain('Coverage: 0 / 7 security domains');
|
|
191
|
+
expect(output).toContain('0 / 7');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// 4. ZAP not applicable when no web target exists
|
|
195
|
+
it('4. ZAP not applicable when no web target exists', () => {
|
|
196
|
+
const scanners: ScannerTelemetry[] = [
|
|
197
|
+
{ scanner: 'OWASP ZAP', state: 'SKIPPED', reason: 'No live web URL provided' }
|
|
198
|
+
];
|
|
199
|
+
|
|
200
|
+
const stats = calculateScore([]);
|
|
201
|
+
const output = captureOutput(() => {
|
|
202
|
+
renderDashboard({
|
|
203
|
+
findings: [],
|
|
204
|
+
stats,
|
|
205
|
+
deterministicScore: dummyDeterministicScore(100, 'A'),
|
|
206
|
+
coverage: perfectCoverage,
|
|
207
|
+
gitInfo: dummyGitInfo,
|
|
208
|
+
duration: '0.1s',
|
|
209
|
+
scanners
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
expect(output).toContain('OWASP ZAP');
|
|
214
|
+
expect(output).toContain('— NOT APPLICABLE');
|
|
215
|
+
expect(output).toContain('(No live web URL provided)');
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// 5. Prowler not applicable when no AWS credentials exist
|
|
219
|
+
it('5. Prowler not applicable when no AWS credentials exist', () => {
|
|
220
|
+
const scanners: ScannerTelemetry[] = [
|
|
221
|
+
{ scanner: 'Prowler', state: 'SKIPPED', reason: 'AWS credentials not configured' }
|
|
222
|
+
];
|
|
223
|
+
|
|
224
|
+
const stats = calculateScore([]);
|
|
225
|
+
const output = captureOutput(() => {
|
|
226
|
+
renderDashboard({
|
|
227
|
+
findings: [],
|
|
228
|
+
stats,
|
|
229
|
+
deterministicScore: dummyDeterministicScore(100, 'A'),
|
|
230
|
+
coverage: perfectCoverage,
|
|
231
|
+
gitInfo: dummyGitInfo,
|
|
232
|
+
duration: '0.1s',
|
|
233
|
+
scanners
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
expect(output).toContain('Prowler');
|
|
238
|
+
expect(output).toContain('— NOT APPLICABLE');
|
|
239
|
+
expect(output).toContain('(AWS credentials not configured)');
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// 6. Findings table formats and truncates properly
|
|
243
|
+
it('6. Findings table formats and truncates properly', () => {
|
|
244
|
+
const manyFindings: NormalizedFinding[] = Array.from({ length: 7 }, (_, i) =>
|
|
245
|
+
createFinding({
|
|
246
|
+
id: `VG-FIND-${String(i + 1).padStart(3, '0')}`,
|
|
247
|
+
scanner: 'Semgrep',
|
|
248
|
+
ruleId: `rule-${i}`,
|
|
249
|
+
title: `This is an extremely long vulnerability title that exceeds column width #${i + 1}`,
|
|
250
|
+
severity: Severity.MEDIUM,
|
|
251
|
+
file: 'src/auth/jwt.ts',
|
|
252
|
+
line: 42 + i
|
|
253
|
+
})
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
const stats = calculateScore(manyFindings);
|
|
257
|
+
const output = captureOutput(() => {
|
|
258
|
+
renderDashboard({
|
|
259
|
+
findings: manyFindings,
|
|
260
|
+
stats,
|
|
261
|
+
deterministicScore: dummyDeterministicScore(79, 'C'),
|
|
262
|
+
coverage: perfectCoverage,
|
|
263
|
+
gitInfo: dummyGitInfo,
|
|
264
|
+
duration: '1.2s',
|
|
265
|
+
scanners: []
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
expect(output).toContain('Showing 5 of 7 findings');
|
|
270
|
+
expect(output).toContain('VG-FIND-001');
|
|
271
|
+
expect(output).toContain('VG-FIND-005');
|
|
272
|
+
expect(output).not.toContain('VG-FIND-006');
|
|
273
|
+
expect(output).toContain('..'); // Truncation mark
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// 7. Deduplicated finding IDs are consistent across runs
|
|
277
|
+
it('7. Deduplicated finding IDs are consistent across runs', () => {
|
|
278
|
+
const findings: NormalizedFinding[] = [
|
|
279
|
+
createFinding({
|
|
280
|
+
id: 'VG-FIND-001',
|
|
281
|
+
scanner: 'npm-audit',
|
|
282
|
+
ruleId: 'GHSA-1',
|
|
283
|
+
title: 'Issue 1',
|
|
284
|
+
severity: Severity.HIGH,
|
|
285
|
+
file: 'package.json',
|
|
286
|
+
line: 10
|
|
287
|
+
}),
|
|
288
|
+
createFinding({
|
|
289
|
+
id: 'VG-FIND-002',
|
|
290
|
+
scanner: 'Semgrep',
|
|
291
|
+
ruleId: 'rules.injection',
|
|
292
|
+
title: 'Issue 2',
|
|
293
|
+
severity: Severity.LOW,
|
|
294
|
+
file: 'src/index.ts',
|
|
295
|
+
line: 25
|
|
296
|
+
})
|
|
297
|
+
];
|
|
298
|
+
|
|
299
|
+
const stats = calculateScore(findings);
|
|
300
|
+
const output = captureOutput(() => {
|
|
301
|
+
renderDashboard({
|
|
302
|
+
findings,
|
|
303
|
+
stats,
|
|
304
|
+
deterministicScore: dummyDeterministicScore(89, 'B'),
|
|
305
|
+
coverage: perfectCoverage,
|
|
306
|
+
gitInfo: dummyGitInfo,
|
|
307
|
+
duration: '1.0s',
|
|
308
|
+
scanners: []
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
expect(output).toContain('VG-FIND-001');
|
|
313
|
+
expect(output).toContain('VG-FIND-002');
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
// 8. Score breakdown math is accurate
|
|
317
|
+
it('8. Score breakdown math is accurate', () => {
|
|
318
|
+
const findings: NormalizedFinding[] = [
|
|
319
|
+
createFinding({ id: 'VG-FIND-001', scanner: 'test', ruleId: 'r1', title: 'High finding', severity: Severity.HIGH, file: 'a.ts', line: 1 }),
|
|
320
|
+
createFinding({ id: 'VG-FIND-002', scanner: 'test', ruleId: 'r2', title: 'Medium finding', severity: Severity.MEDIUM, file: 'b.ts', line: 2 }),
|
|
321
|
+
createFinding({ id: 'VG-FIND-003', scanner: 'test', ruleId: 'r3', title: 'Low finding', severity: Severity.LOW, file: 'c.ts', line: 3 })
|
|
322
|
+
];
|
|
323
|
+
|
|
324
|
+
const stats = calculateScore(findings);
|
|
325
|
+
// Base 100 - (10 + 3 + 1) = 86, Grade B
|
|
326
|
+
expect(stats.score).toBe(86);
|
|
327
|
+
expect(stats.grade).toBe('B');
|
|
328
|
+
|
|
329
|
+
const scoreObj = dummyDeterministicScore(86, 'B', {
|
|
330
|
+
deductions: { critical: 0, high: 10, medium: 3, low: 1, info: 0, totalDeductions: 14 },
|
|
331
|
+
breakdown: { critical: 0, high: 1, medium: 1, low: 1, info: 0 }
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
const output = captureOutput(() => {
|
|
335
|
+
renderDashboard({
|
|
336
|
+
findings,
|
|
337
|
+
stats,
|
|
338
|
+
deterministicScore: scoreObj,
|
|
339
|
+
coverage: perfectCoverage,
|
|
340
|
+
gitInfo: dummyGitInfo,
|
|
341
|
+
duration: '1.0s',
|
|
342
|
+
scanners: []
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
expect(output).toMatch(/Base score\s+100/);
|
|
347
|
+
expect(output).toMatch(/1 × High finding\s+- 10/);
|
|
348
|
+
expect(output).toMatch(/1 × Medium finding\s+- 3/);
|
|
349
|
+
expect(output).toMatch(/1 × Low finding\s+- 1/);
|
|
350
|
+
expect(output).toMatch(/Final score\s+86/);
|
|
351
|
+
expect(output).toMatch(/Grade\s+B/);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// 9. Critical findings cap grade at F
|
|
355
|
+
it('9. Critical findings cap grade at F', () => {
|
|
356
|
+
const findings: NormalizedFinding[] = [
|
|
357
|
+
createFinding({ id: 'VG-FIND-001', scanner: 'gitleaks', ruleId: 'secrets', title: 'Hardcoded Secret', severity: Severity.CRITICAL, file: 'config.ts', line: 5 })
|
|
358
|
+
];
|
|
359
|
+
|
|
360
|
+
const stats = calculateScore(findings);
|
|
361
|
+
expect(stats.grade).toBe('F');
|
|
362
|
+
expect(stats.score).toBeLessThanOrEqual(49);
|
|
363
|
+
|
|
364
|
+
const scoreObj = dummyDeterministicScore(49, 'F', {
|
|
365
|
+
deductions: { critical: 30, high: 0, medium: 0, low: 0, info: 0, totalDeductions: 30 },
|
|
366
|
+
breakdown: { critical: 1, high: 0, medium: 0, low: 0, info: 0 },
|
|
367
|
+
explanation: ['Grade Override: F']
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
const output = captureOutput(() => {
|
|
371
|
+
renderDashboard({
|
|
372
|
+
findings,
|
|
373
|
+
stats,
|
|
374
|
+
deterministicScore: scoreObj,
|
|
375
|
+
coverage: perfectCoverage,
|
|
376
|
+
gitInfo: dummyGitInfo,
|
|
377
|
+
duration: '0.5s',
|
|
378
|
+
scanners: []
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
expect(output).toContain('Critical finding detected: Grade capped at F');
|
|
383
|
+
expect(output).toContain('Grade F');
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
// 10. AI remediation unavailable message when API key missing
|
|
387
|
+
it('10. AI remediation unavailable message when API key missing', () => {
|
|
388
|
+
const remediation: AIRemediationData = {
|
|
389
|
+
findingId: 'VG-FIND-001',
|
|
390
|
+
issue: 'Exposed API token',
|
|
391
|
+
hasConcretePatch: false,
|
|
392
|
+
status: 'UNAVAILABLE',
|
|
393
|
+
unavailableReason: 'NVIDIA_API_KEY not configured.'
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
const stats = calculateScore([]);
|
|
397
|
+
const output = captureOutput(() => {
|
|
398
|
+
renderDashboard({
|
|
399
|
+
findings: [],
|
|
400
|
+
stats,
|
|
401
|
+
deterministicScore: dummyDeterministicScore(100, 'A'),
|
|
402
|
+
coverage: perfectCoverage,
|
|
403
|
+
gitInfo: dummyGitInfo,
|
|
404
|
+
duration: '0.5s',
|
|
405
|
+
scanners: [],
|
|
406
|
+
remediation
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
expect(output).toContain('▶ AI REMEDIATION · UNAVAILABLE');
|
|
411
|
+
expect(output).toContain('NVIDIA_API_KEY not configured.');
|
|
412
|
+
expect(output).toContain('Scanning, deterministic scoring, and policy enforcement remain 100% operational.');
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
// 11. AI remediation with valid response renders structured blocks
|
|
416
|
+
it('11. AI remediation with valid response renders structured blocks', () => {
|
|
417
|
+
const remediation: AIRemediationData = {
|
|
418
|
+
findingId: 'VG-FIND-001',
|
|
419
|
+
severity: 'HIGH',
|
|
420
|
+
issue: 'Prototype Pollution vulnerability in lodash',
|
|
421
|
+
impact: 'Attacker may inject arbitrary properties into Object.prototype',
|
|
422
|
+
recommendation: 'Upgrade lodash to >= 4.17.21',
|
|
423
|
+
suggestedFix: '- "lodash": "4.17.15"\n+ "lodash": "4.17.21"',
|
|
424
|
+
hasConcretePatch: true,
|
|
425
|
+
confidence: 90,
|
|
426
|
+
status: 'AWAITING REVIEW'
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
const stats = calculateScore([]);
|
|
430
|
+
const output = captureOutput(() => {
|
|
431
|
+
renderDashboard({
|
|
432
|
+
findings: [],
|
|
433
|
+
stats,
|
|
434
|
+
deterministicScore: dummyDeterministicScore(90, 'A'),
|
|
435
|
+
coverage: perfectCoverage,
|
|
436
|
+
gitInfo: dummyGitInfo,
|
|
437
|
+
duration: '1.0s',
|
|
438
|
+
scanners: [],
|
|
439
|
+
remediation
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
expect(output).toContain('▶ AI REMEDIATION · OPTIONAL');
|
|
444
|
+
expect(output).toContain('ISSUE');
|
|
445
|
+
expect(output).toContain('Prototype Pollution vulnerability in lodash');
|
|
446
|
+
expect(output).toContain('IMPACT');
|
|
447
|
+
expect(output).toContain('Attacker may inject arbitrary properties into Object.prototype');
|
|
448
|
+
expect(output).toContain('RECOMMENDED FIX');
|
|
449
|
+
expect(output).toContain('Upgrade lodash to >= 4.17.21');
|
|
450
|
+
expect(output).toContain('SUGGESTED FIX');
|
|
451
|
+
expect(output).toContain('Confidence: 90%');
|
|
452
|
+
expect(output).toContain('Status: AWAITING REVIEW');
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
// 12. AI remediation with malformed JSON falls back gracefully to guidance
|
|
456
|
+
it('12. AI remediation with malformed JSON falls back gracefully to guidance', () => {
|
|
457
|
+
const remediation: AIRemediationData = {
|
|
458
|
+
findingId: 'VG-FIND-001',
|
|
459
|
+
severity: 'MEDIUM',
|
|
460
|
+
issue: 'SQL parameterization advice',
|
|
461
|
+
impact: 'Potential data leakage',
|
|
462
|
+
recommendation: 'Use parameterized queries instead of string concatenation',
|
|
463
|
+
hasConcretePatch: false,
|
|
464
|
+
status: 'GUIDANCE ONLY'
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
const stats = calculateScore([]);
|
|
468
|
+
const output = captureOutput(() => {
|
|
469
|
+
renderDashboard({
|
|
470
|
+
findings: [],
|
|
471
|
+
stats,
|
|
472
|
+
deterministicScore: dummyDeterministicScore(95, 'A'),
|
|
473
|
+
coverage: perfectCoverage,
|
|
474
|
+
gitInfo: dummyGitInfo,
|
|
475
|
+
duration: '1.0s',
|
|
476
|
+
scanners: [],
|
|
477
|
+
remediation
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
expect(output).toContain('RECOMMENDED FIX');
|
|
482
|
+
expect(output).toContain('Use parameterized queries instead of string concatenation');
|
|
483
|
+
expect(output).toContain('No patch generated — guidance only.');
|
|
484
|
+
expect(output).toContain('Status: GUIDANCE ONLY');
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
// 13. AI remediation with no patch renders GUIDANCE ONLY without patch box
|
|
488
|
+
it('13. AI remediation with no patch renders GUIDANCE ONLY without patch box', () => {
|
|
489
|
+
const remediation: AIRemediationData = {
|
|
490
|
+
findingId: 'VG-FIND-002',
|
|
491
|
+
issue: 'Missing security header',
|
|
492
|
+
hasConcretePatch: false,
|
|
493
|
+
status: 'GUIDANCE ONLY'
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
const stats = calculateScore([]);
|
|
497
|
+
const output = captureOutput(() => {
|
|
498
|
+
renderDashboard({
|
|
499
|
+
findings: [],
|
|
500
|
+
stats,
|
|
501
|
+
deterministicScore: dummyDeterministicScore(97, 'A'),
|
|
502
|
+
coverage: perfectCoverage,
|
|
503
|
+
gitInfo: dummyGitInfo,
|
|
504
|
+
duration: '0.4s',
|
|
505
|
+
scanners: [],
|
|
506
|
+
remediation
|
|
507
|
+
});
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
expect(output).toContain('No patch generated — guidance only.');
|
|
511
|
+
expect(output).toContain('Status: GUIDANCE ONLY');
|
|
512
|
+
expect(output).not.toContain('Confidence:');
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
// 14. CI mode outputs expected PASS/FAIL format
|
|
516
|
+
it('14. CI mode outputs expected PASS/FAIL format', () => {
|
|
517
|
+
const scoreObj = dummyDeterministicScore(100, 'A');
|
|
518
|
+
const passLines = renderCIOutput({
|
|
519
|
+
deterministicScore: scoreObj,
|
|
520
|
+
findings: [],
|
|
521
|
+
policyPassed: true,
|
|
522
|
+
failThreshold: 'high'
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
const passOutput = passLines.join('\n');
|
|
526
|
+
expect(passOutput).toContain('VibeGuard Security Policy');
|
|
527
|
+
expect(passOutput).toContain('Score: 100/100 (A)');
|
|
528
|
+
expect(passOutput).toContain('Policy: PASS');
|
|
529
|
+
expect(passOutput).toContain('Threshold: HIGH');
|
|
530
|
+
|
|
531
|
+
const failScoreObj = dummyDeterministicScore(70, 'C', {
|
|
532
|
+
deductions: { critical: 0, high: 20, medium: 0, low: 0, info: 0, totalDeductions: 20 },
|
|
533
|
+
breakdown: { critical: 0, high: 2, medium: 0, low: 0, info: 0 }
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
const failLines = renderCIOutput({
|
|
537
|
+
deterministicScore: failScoreObj,
|
|
538
|
+
findings: [
|
|
539
|
+
createFinding({ id: 'VG-FIND-001', scanner: 'test', ruleId: 'r1', title: 'h1', severity: Severity.HIGH, file: 'a.ts', line: 1 }),
|
|
540
|
+
createFinding({ id: 'VG-FIND-002', scanner: 'test', ruleId: 'r2', title: 'h2', severity: Severity.HIGH, file: 'b.ts', line: 2 })
|
|
541
|
+
],
|
|
542
|
+
policyPassed: false,
|
|
543
|
+
failThreshold: 'high'
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
const failOutput = failLines.join('\n');
|
|
547
|
+
expect(failOutput).toContain('Score: 70/100 (C)');
|
|
548
|
+
expect(failOutput).toContain('Policy: FAIL');
|
|
549
|
+
expect(failOutput).toContain('High: 2');
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
// 15. JSON mode outputs valid JSON with expected schema
|
|
553
|
+
it('15. JSON mode outputs valid JSON with expected schema', () => {
|
|
554
|
+
const jsonResult = generateJsonOutput({
|
|
555
|
+
deterministicScore: dummyDeterministicScore(100, 'A'),
|
|
556
|
+
findings: [
|
|
557
|
+
createFinding({
|
|
558
|
+
id: 'VG-FIND-001',
|
|
559
|
+
title: 'Exposed credentials',
|
|
560
|
+
severity: Severity.CRITICAL,
|
|
561
|
+
scanner: 'Gitleaks',
|
|
562
|
+
file: '.env',
|
|
563
|
+
line: 3,
|
|
564
|
+
ruleId: 'generic-api-key',
|
|
565
|
+
cwe: 'CWE-798',
|
|
566
|
+
owasp: 'A07:2021'
|
|
567
|
+
})
|
|
568
|
+
],
|
|
569
|
+
coverageData: perfectCoverage,
|
|
570
|
+
activeDomainsCount: 7,
|
|
571
|
+
postureStatus: 'COMPLETE',
|
|
572
|
+
scanners: [{ scanner: 'Gitleaks', state: 'SUCCESS', durationMs: 250, findingsCount: 1 }],
|
|
573
|
+
gitInfo: dummyGitInfo,
|
|
574
|
+
policyPassed: false,
|
|
575
|
+
failThreshold: 'critical',
|
|
576
|
+
durationMs: 1200
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
const serialized = JSON.stringify(jsonResult);
|
|
580
|
+
expect(() => JSON.parse(serialized)).not.toThrow();
|
|
581
|
+
|
|
582
|
+
const parsed = JSON.parse(serialized);
|
|
583
|
+
expect(parsed.score).toBe(100);
|
|
584
|
+
expect(parsed.grade).toBe('A');
|
|
585
|
+
expect(parsed.postureStatus).toBe('COMPLETE');
|
|
586
|
+
expect(parsed.coverage.assessedDomains).toBe(7);
|
|
587
|
+
expect(parsed.coverage.totalDomains).toBe(7);
|
|
588
|
+
expect(parsed.findings).toHaveLength(1);
|
|
589
|
+
expect(parsed.findings[0].id).toBe('VG-FIND-001');
|
|
590
|
+
expect(parsed.repository.name).toBe('VibeGuard');
|
|
591
|
+
expect(parsed.policyResult.passed).toBe(false);
|
|
592
|
+
expect(parsed.policyResult.threshold).toBe('CRITICAL');
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
// 16. Exit codes match policy results (0 for pass, 1 for fail, 2 for error)
|
|
596
|
+
it('16. Exit codes match policy results (0 for pass, 1 for fail, 2 for error)', () => {
|
|
597
|
+
// Threshold HIGH: passes if no critical or high
|
|
598
|
+
const resPass = evaluatePolicy('high', { critical: 0, high: 0, medium: 2, low: 1 }, 3);
|
|
599
|
+
expect(resPass.passed).toBe(true);
|
|
600
|
+
expect(resPass.exitCode).toBe(0);
|
|
601
|
+
|
|
602
|
+
// Threshold HIGH: fails if high exists
|
|
603
|
+
const resFailHigh = evaluatePolicy('high', { critical: 0, high: 1, medium: 0, low: 0 }, 1);
|
|
604
|
+
expect(resFailHigh.passed).toBe(false);
|
|
605
|
+
expect(resFailHigh.exitCode).toBe(1);
|
|
606
|
+
|
|
607
|
+
// Threshold CRITICAL: passes if only high exists
|
|
608
|
+
const resPassCritical = evaluatePolicy('critical', { critical: 0, high: 2, medium: 1, low: 0 }, 3);
|
|
609
|
+
expect(resPassCritical.passed).toBe(true);
|
|
610
|
+
expect(resPassCritical.exitCode).toBe(0);
|
|
611
|
+
|
|
612
|
+
// Threshold CRITICAL: fails if critical exists
|
|
613
|
+
const resFailCritical = evaluatePolicy('critical', { critical: 1, high: 0, medium: 0, low: 0 }, 1);
|
|
614
|
+
expect(resFailCritical.passed).toBe(false);
|
|
615
|
+
expect(resFailCritical.exitCode).toBe(1);
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
// 17. No secrets are leaked in CLI output under any condition
|
|
619
|
+
it('17. No secrets are leaked in CLI output under any condition', () => {
|
|
620
|
+
const rawAwsKey = 'AKIA1234567890ABCDEF';
|
|
621
|
+
const rawGithubToken = 'ghp_abcdefghijklmnopqrstuvwxyz1234567890';
|
|
622
|
+
const rawSecretAssignment = 'api_key: "super_secret_token_12345"';
|
|
623
|
+
|
|
624
|
+
// Test maskSecrets directly
|
|
625
|
+
expect(maskSecrets(rawAwsKey)).toBe('AKIA****************');
|
|
626
|
+
expect(maskSecrets(rawGithubToken)).toBe('ghp_************************************');
|
|
627
|
+
expect(maskSecrets(rawSecretAssignment)).toBe('api_key: [REDACTED]');
|
|
628
|
+
|
|
629
|
+
// Test renderDashboard with a finding containing secrets
|
|
630
|
+
const secretFinding = createFinding({
|
|
631
|
+
id: 'VG-FIND-001',
|
|
632
|
+
scanner: 'Gitleaks',
|
|
633
|
+
ruleId: 'aws-access-token',
|
|
634
|
+
title: `Leaked key ${rawAwsKey}`,
|
|
635
|
+
severity: Severity.CRITICAL,
|
|
636
|
+
file: 'credentials.json',
|
|
637
|
+
line: 4
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
const stats = calculateScore([secretFinding]);
|
|
641
|
+
const output = captureOutput(() => {
|
|
642
|
+
renderDashboard({
|
|
643
|
+
findings: [secretFinding],
|
|
644
|
+
stats,
|
|
645
|
+
deterministicScore: dummyDeterministicScore(49, 'F'),
|
|
646
|
+
coverage: perfectCoverage,
|
|
647
|
+
gitInfo: dummyGitInfo,
|
|
648
|
+
duration: '0.4s',
|
|
649
|
+
scanners: [],
|
|
650
|
+
remediation: {
|
|
651
|
+
findingId: 'VG-FIND-001',
|
|
652
|
+
issue: `Found token ${rawGithubToken}`,
|
|
653
|
+
suggestedFix: `Remove ${rawAwsKey}`,
|
|
654
|
+
hasConcretePatch: true,
|
|
655
|
+
status: 'AWAITING REVIEW'
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
// Ensure raw secrets do NOT appear anywhere in the output
|
|
661
|
+
expect(output).not.toContain(rawAwsKey);
|
|
662
|
+
expect(output).not.toContain(rawGithubToken);
|
|
663
|
+
expect(output).toContain('AKIA****************');
|
|
664
|
+
expect(output).toContain('ghp_************************************');
|
|
665
|
+
});
|
|
666
|
+
});
|