@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.
@@ -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
+ });