@maverick006/vibeguard 1.0.11 → 1.0.14
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 +364 -120
- package/dist/index.js +317 -106
- package/jest.config.js +5 -0
- package/package.json +32 -28
- package/src/credentials.ts +74 -0
- package/src/formatter.ts +445 -142
- package/src/index.ts +456 -211
- package/tests/auth.test.ts +96 -0
- package/tests/formatter.test.ts +666 -0
package/src/formatter.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { execSync } from 'child_process';
|
|
3
|
-
import { NormalizedFinding, Severity } from '@maverick006/types';
|
|
3
|
+
import { NormalizedFinding, Severity, ScannerCoverage, ScannerState, DeterministicScore } from '@maverick006/types';
|
|
4
4
|
import path from 'path';
|
|
5
5
|
|
|
6
6
|
export interface ScanStats {
|
|
@@ -8,11 +8,59 @@ export interface ScanStats {
|
|
|
8
8
|
high: number;
|
|
9
9
|
medium: number;
|
|
10
10
|
low: number;
|
|
11
|
+
info: number;
|
|
11
12
|
total: number;
|
|
12
13
|
score: number;
|
|
14
|
+
grade: 'A' | 'B' | 'C' | 'D' | 'F';
|
|
13
15
|
riskLevel: 'LOW RISK' | 'MEDIUM RISK' | 'HIGH RISK' | 'CRITICAL RISK';
|
|
14
16
|
}
|
|
15
17
|
|
|
18
|
+
export interface AIRemediationData {
|
|
19
|
+
findingId: string;
|
|
20
|
+
severity?: string;
|
|
21
|
+
issue: string;
|
|
22
|
+
impact?: string;
|
|
23
|
+
recommendation?: string;
|
|
24
|
+
suggestedFix?: string;
|
|
25
|
+
hasConcretePatch: boolean;
|
|
26
|
+
confidence?: number;
|
|
27
|
+
status: 'AWAITING REVIEW' | 'GUIDANCE ONLY' | 'VERIFIED' | 'NOT_VERIFIED' | 'UNAVAILABLE';
|
|
28
|
+
unavailableReason?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ScannerTelemetry {
|
|
32
|
+
scanner: string;
|
|
33
|
+
state: ScannerState | string;
|
|
34
|
+
durationMs?: number;
|
|
35
|
+
findingsCount?: number;
|
|
36
|
+
reason?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface RenderOptions {
|
|
40
|
+
findings: NormalizedFinding[];
|
|
41
|
+
stats: ScanStats;
|
|
42
|
+
deterministicScore?: DeterministicScore;
|
|
43
|
+
coverage: ScannerCoverage;
|
|
44
|
+
gitInfo: ReturnType<typeof getGitInfo>;
|
|
45
|
+
duration: string;
|
|
46
|
+
scanners: ScannerTelemetry[];
|
|
47
|
+
remediation?: AIRemediationData;
|
|
48
|
+
syncStatus?: 'SYNCED' | 'SKIPPED' | 'FAILED';
|
|
49
|
+
policyThreshold?: string;
|
|
50
|
+
verbose?: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function maskSecrets(input: string): string {
|
|
54
|
+
if (!input) return input;
|
|
55
|
+
let masked = input;
|
|
56
|
+
masked = masked.replace(/\b(AKIA[0-9A-Z]{16})\b/g, 'AKIA****************');
|
|
57
|
+
masked = masked.replace(/\b(gh[pousr]_[A-Za-z0-9_]{36,255})\b/g, 'ghp_************************************');
|
|
58
|
+
masked = masked.replace(/(bearer\s+)([a-zA-Z0-9_\-\.]{15,})/gi, '$1[REDACTED]');
|
|
59
|
+
masked = masked.replace(/(password|secret|api[_-]?key)\s*[:=]\s*['"]?([a-zA-Z0-9_\-\.]{8,})['"]?/gi, '$1: [REDACTED]');
|
|
60
|
+
masked = masked.replace(/-----BEGIN[ A-Z0-9_-]+PRIVATE KEY-----[\s\S]*?-----END[ A-Z0-9_-]+PRIVATE KEY-----/g, '[REDACTED_PRIVATE_KEY]');
|
|
61
|
+
return masked;
|
|
62
|
+
}
|
|
63
|
+
|
|
16
64
|
function stripAnsi(str: string): string {
|
|
17
65
|
return str.replace(/\u001b\[[0-9;]*m/g, '');
|
|
18
66
|
}
|
|
@@ -22,8 +70,7 @@ function visibleWidth(str: string): number {
|
|
|
22
70
|
let width = 0;
|
|
23
71
|
for (const char of plain) {
|
|
24
72
|
const code = char.codePointAt(0) || 0;
|
|
25
|
-
if (code === 0xFE0F || code === 0xFE0E) continue;
|
|
26
|
-
// Emojis and certain symbols take 2 terminal columns
|
|
73
|
+
if (code === 0xFE0F || code === 0xFE0E) continue;
|
|
27
74
|
if (code > 0x1F000 || (code >= 0x2600 && code <= 0x27BF)) {
|
|
28
75
|
width += 2;
|
|
29
76
|
} else {
|
|
@@ -43,7 +90,7 @@ export function getGitInfo(cwd: string = process.cwd()) {
|
|
|
43
90
|
const resolvedCwd = path.resolve(cwd);
|
|
44
91
|
let name = path.basename(resolvedCwd);
|
|
45
92
|
let branch = 'main';
|
|
46
|
-
let commit = '
|
|
93
|
+
let commit = 'HEAD';
|
|
47
94
|
let remoteUrl = '';
|
|
48
95
|
|
|
49
96
|
try {
|
|
@@ -65,7 +112,7 @@ export function getGitInfo(cwd: string = process.cwd()) {
|
|
|
65
112
|
}
|
|
66
113
|
} catch {}
|
|
67
114
|
|
|
68
|
-
return { name, branch, commit, remoteUrl
|
|
115
|
+
return { name, branch, commit, remoteUrl };
|
|
69
116
|
}
|
|
70
117
|
|
|
71
118
|
export function calculateScore(findings: NormalizedFinding[]): ScanStats {
|
|
@@ -73,50 +120,62 @@ export function calculateScore(findings: NormalizedFinding[]): ScanStats {
|
|
|
73
120
|
let high = 0;
|
|
74
121
|
let medium = 0;
|
|
75
122
|
let low = 0;
|
|
123
|
+
let info = 0;
|
|
76
124
|
|
|
77
125
|
for (const f of findings) {
|
|
78
126
|
const sev = (f.severity || '').toUpperCase();
|
|
79
127
|
if (sev === 'CRITICAL' || sev === Severity.CRITICAL) critical++;
|
|
80
128
|
else if (sev === 'HIGH' || sev === Severity.HIGH) high++;
|
|
81
129
|
else if (sev === 'MEDIUM' || sev === Severity.MEDIUM) medium++;
|
|
82
|
-
else low++;
|
|
130
|
+
else if (sev === 'LOW' || sev === Severity.LOW) low++;
|
|
131
|
+
else info++;
|
|
83
132
|
}
|
|
84
133
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
134
|
+
const deductions = (critical * 30) + (high * 10) + (medium * 3) + (low * 1);
|
|
135
|
+
let score = Math.max(0, Math.min(100, 100 - deductions));
|
|
136
|
+
|
|
137
|
+
let grade: ScanStats['grade'] = 'A';
|
|
138
|
+
if (critical > 0) {
|
|
139
|
+
grade = 'F';
|
|
140
|
+
score = Math.min(score, 49);
|
|
141
|
+
} else if (score >= 90) grade = 'A';
|
|
142
|
+
else if (score >= 80) grade = 'B';
|
|
143
|
+
else if (score >= 70) grade = 'C';
|
|
144
|
+
else if (score >= 50) grade = 'D';
|
|
145
|
+
else grade = 'F';
|
|
88
146
|
|
|
89
147
|
let riskLevel: ScanStats['riskLevel'] = 'LOW RISK';
|
|
90
|
-
if (
|
|
91
|
-
else if (
|
|
92
|
-
else if (
|
|
148
|
+
if (grade === 'F') riskLevel = 'CRITICAL RISK';
|
|
149
|
+
else if (grade === 'D') riskLevel = 'HIGH RISK';
|
|
150
|
+
else if (grade === 'C' || grade === 'B') riskLevel = 'MEDIUM RISK';
|
|
93
151
|
|
|
94
152
|
return {
|
|
95
153
|
critical,
|
|
96
154
|
high,
|
|
97
155
|
medium,
|
|
98
156
|
low,
|
|
157
|
+
info,
|
|
99
158
|
total: findings.length,
|
|
100
159
|
score,
|
|
160
|
+
grade,
|
|
101
161
|
riskLevel
|
|
102
162
|
};
|
|
103
163
|
}
|
|
104
164
|
|
|
105
|
-
export function renderDashboard(options: {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
119
|
-
const { findings, stats, gitInfo, duration, remediation } = options;
|
|
165
|
+
export function renderDashboard(options: RenderOptions) {
|
|
166
|
+
const {
|
|
167
|
+
findings,
|
|
168
|
+
stats,
|
|
169
|
+
deterministicScore,
|
|
170
|
+
coverage,
|
|
171
|
+
gitInfo,
|
|
172
|
+
duration,
|
|
173
|
+
scanners,
|
|
174
|
+
remediation,
|
|
175
|
+
syncStatus,
|
|
176
|
+
policyThreshold,
|
|
177
|
+
verbose
|
|
178
|
+
} = options;
|
|
120
179
|
|
|
121
180
|
const cyan = chalk.hex('#00E5FF');
|
|
122
181
|
const gray = chalk.hex('#94A3B8');
|
|
@@ -128,20 +187,20 @@ export function renderDashboard(options: {
|
|
|
128
187
|
const yellow = chalk.hex('#F59E0B');
|
|
129
188
|
const white = chalk.white;
|
|
130
189
|
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
'
|
|
137
|
-
'
|
|
138
|
-
'
|
|
139
|
-
'
|
|
140
|
-
' \\ \\ / / ',
|
|
141
|
-
' `-------\' '
|
|
190
|
+
// Active domains count (out of 7)
|
|
191
|
+
const domainKeys: (keyof ScannerCoverage)[] = [
|
|
192
|
+
'code',
|
|
193
|
+
'dependencies',
|
|
194
|
+
'secrets',
|
|
195
|
+
'containers',
|
|
196
|
+
'iac',
|
|
197
|
+
'web',
|
|
198
|
+
'cloud'
|
|
142
199
|
];
|
|
200
|
+
const activeDomainsCount = domainKeys.filter(k => coverage[k]).length;
|
|
201
|
+
const isPartial = activeDomainsCount < 7;
|
|
143
202
|
|
|
144
|
-
// Title ASCII Art
|
|
203
|
+
// Title ASCII Art
|
|
145
204
|
const title = [
|
|
146
205
|
' __ __ ___ ____ _____ ____ _ _ _ ____ ____ ',
|
|
147
206
|
'\\ \\ / /|_ _| __ )| ____/ ___| | | | / \\ | _ \\| _ \\ ',
|
|
@@ -150,127 +209,371 @@ export function renderDashboard(options: {
|
|
|
150
209
|
' \\_/ |___|____/|_____|\\____|\\___/_/ \\_\\_| \\_\\____/'
|
|
151
210
|
];
|
|
152
211
|
|
|
153
|
-
const scoreColor = stats.score >= 85 ? green : stats.score >= 65 ? yellow : red;
|
|
154
|
-
const riskColor = stats.riskLevel === 'LOW RISK' ? green : stats.riskLevel === 'MEDIUM RISK' ? yellow : red;
|
|
155
|
-
|
|
156
212
|
console.log('\n');
|
|
157
|
-
|
|
158
|
-
//
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
console.log(
|
|
163
|
-
console.log(
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
console.log(
|
|
168
|
-
console.log(
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
213
|
+
|
|
214
|
+
// 1. Header (VIBEGUARD - Cloud + Security Posture)
|
|
215
|
+
for (const line of title) {
|
|
216
|
+
console.log(cyan.bold(line));
|
|
217
|
+
}
|
|
218
|
+
console.log(`\n${white.bold('VIBEGUARD')} ${dimGray('│')} ${cyan('Cloud + Security Posture')}`);
|
|
219
|
+
console.log(gray('Scanning. Analyzing. Protecting.\n'));
|
|
220
|
+
|
|
221
|
+
// 2. Redesigned Security Posture Header (Honest Partial Posture Representation)
|
|
222
|
+
const boxWidth = 76;
|
|
223
|
+
console.log(darkBorder(`┌─ SECURITY POSTURE ${'─'.repeat(boxWidth - 21)}┐`));
|
|
224
|
+
console.log(`${darkBorder('│')}${' '.repeat(boxWidth)}${darkBorder('│')}`);
|
|
225
|
+
|
|
226
|
+
const scoreNum = deterministicScore?.score ?? stats.score;
|
|
227
|
+
const gradeLetter = deterministicScore?.grade ?? stats.grade;
|
|
228
|
+
const gradeColor = gradeLetter === 'A' ? green : gradeLetter === 'B' ? cyan : gradeLetter === 'C' ? yellow : red;
|
|
229
|
+
|
|
230
|
+
const scoreLine = ` ${gradeColor.bold(String(scoreNum))} ${gray('/ 100')} ${gradeColor.bold('Grade ' + gradeLetter)}`;
|
|
231
|
+
console.log(`${darkBorder('│')}${padVisible(scoreLine, boxWidth)}${darkBorder('│')}`);
|
|
232
|
+
|
|
233
|
+
const postureBadge = isPartial
|
|
234
|
+
? yellow.bold('PARTIAL POSTURE') + dimGray(` (${activeDomainsCount} / 7 security domains assessed)`)
|
|
235
|
+
: green.bold('COMPLETE POSTURE') + dimGray(' (7 / 7 security domains assessed)');
|
|
236
|
+
const statusLine = ` ${postureBadge}`;
|
|
237
|
+
console.log(`${darkBorder('│')}${padVisible(statusLine, boxWidth)}${darkBorder('│')}`);
|
|
174
238
|
console.log(`${darkBorder('│')}${' '.repeat(boxWidth)}${darkBorder('│')}`);
|
|
175
|
-
|
|
176
|
-
const scoreLine1 = ` ${scoreColor.bold(String(stats.score))} ${gray('/100')} ${red('CRITICAL')} ${String(stats.critical).padEnd(2, ' ')} ${orange('HIGH')} ${String(stats.high).padEnd(2, ' ')}`;
|
|
177
|
-
const scoreLine2 = ` ${riskColor.bold(stats.riskLevel.padEnd(13, ' '))} ${yellow('MEDIUM')} ${String(stats.medium).padEnd(2, ' ')} ${cyan('LOW')} ${String(stats.low).padEnd(2, ' ')}`;
|
|
178
239
|
|
|
179
|
-
|
|
180
|
-
|
|
240
|
+
const critStr = stats.critical > 0 ? red.bold(`${stats.critical} Critical`) : dimGray('0 Critical');
|
|
241
|
+
const highStr = stats.high > 0 ? orange.bold(`${stats.high} High`) : dimGray('0 High');
|
|
242
|
+
const medStr = stats.medium > 0 ? yellow.bold(`${stats.medium} Medium`) : dimGray('0 Medium');
|
|
243
|
+
const lowStr = stats.low > 0 ? cyan(`${stats.low} Low`) : dimGray('0 Low');
|
|
244
|
+
const findingsLine = ` ${critStr} ${highStr} ${medStr} ${lowStr} ${gray(`(${stats.total} total ${stats.total === 1 ? 'finding' : 'findings'})`)}`;
|
|
245
|
+
console.log(`${darkBorder('│')}${padVisible(findingsLine, boxWidth)}${darkBorder('│')}`);
|
|
246
|
+
|
|
247
|
+
const coverageLine = ` Coverage: ${cyan(`${activeDomainsCount} / 7`)} security domains`;
|
|
248
|
+
console.log(`${darkBorder('│')}${padVisible(coverageLine, boxWidth)}${darkBorder('│')}`);
|
|
181
249
|
console.log(darkBorder(`└${'─'.repeat(boxWidth)}┘`));
|
|
182
250
|
console.log('');
|
|
183
251
|
|
|
184
|
-
// 3.
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
252
|
+
// 3. Truthful Scanner Status & Security Domains
|
|
253
|
+
console.log(cyan.bold('▶ SCANNER COVERAGE'));
|
|
254
|
+
console.log('');
|
|
255
|
+
|
|
256
|
+
for (const s of scanners) {
|
|
257
|
+
const dur = s.durationMs ? `${(s.durationMs / 1000).toFixed(1)}s` : '0.1s';
|
|
258
|
+
const name = s.scanner.padEnd(16);
|
|
259
|
+
|
|
260
|
+
let stateStr = '';
|
|
261
|
+
const normState = String(s.state).toUpperCase();
|
|
262
|
+
|
|
263
|
+
if (normState === 'SUCCESS') {
|
|
264
|
+
const count = s.findingsCount || 0;
|
|
265
|
+
const countLabel = `${count} ${count === 1 ? 'finding' : 'findings'}`;
|
|
266
|
+
stateStr = `${green('✓ SUCCESS')} ${white(countLabel.padEnd(14))} ${dimGray(dur)}`;
|
|
267
|
+
} else if (normState === 'NOT_INSTALLED') {
|
|
268
|
+
stateStr = `${dimGray('○ NOT INSTALLED')}`;
|
|
269
|
+
} else if (normState === 'SKIPPED') {
|
|
270
|
+
const r = (s.reason || '').toLowerCase();
|
|
271
|
+
if (r.includes('not applicable') || r.includes('no live web') || r.includes('no iac') || r.includes('credentials not configured')) {
|
|
272
|
+
stateStr = `${dimGray('— NOT APPLICABLE')} ${dimGray(s.reason ? `(${s.reason.replace(/^Skipped:\s*/i, '')})` : '')}`;
|
|
273
|
+
} else {
|
|
274
|
+
stateStr = `${yellow('⚠ SKIPPED')} ${dimGray(s.reason || '')}`;
|
|
275
|
+
}
|
|
276
|
+
} else if (normState === 'TIMEOUT') {
|
|
277
|
+
stateStr = `${red('⏱ TIMEOUT')} ${dimGray(`(${dur})`)}`;
|
|
278
|
+
} else if (normState === 'FAILED') {
|
|
279
|
+
stateStr = `${red('✗ FAILED')} ${dimGray(s.reason || '')}`;
|
|
280
|
+
} else {
|
|
281
|
+
stateStr = `${dimGray('— ' + normState)}`;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
console.log(` ${name} ${stateStr}`);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Domain mapping summary
|
|
288
|
+
console.log(`\n ${gray(`Domain Assessment: ${activeDomainsCount} / 7`)}`);
|
|
289
|
+
const domainLabels: { key: keyof ScannerCoverage; label: string }[] = [
|
|
290
|
+
{ key: 'dependencies', label: 'Dependencies' },
|
|
291
|
+
{ key: 'code', label: 'Code' },
|
|
292
|
+
{ key: 'secrets', label: 'Secrets' },
|
|
293
|
+
{ key: 'containers', label: 'Containers' },
|
|
294
|
+
{ key: 'iac', label: 'IaC' },
|
|
295
|
+
{ key: 'web', label: 'Web/API' },
|
|
296
|
+
{ key: 'cloud', label: 'Cloud' }
|
|
199
297
|
];
|
|
200
298
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
299
|
+
const domainStr = domainLabels
|
|
300
|
+
.map(d => (coverage[d.key] ? green(`✓ ${d.label}`) : dimGray(`○ ${d.label}`)))
|
|
301
|
+
.join(' ');
|
|
302
|
+
console.log(` ${domainStr}\n`);
|
|
303
|
+
|
|
304
|
+
// 4. Score Breakdown
|
|
305
|
+
console.log(cyan.bold('▶ SCORE BREAKDOWN'));
|
|
306
|
+
console.log('');
|
|
307
|
+
const breakdownWidth = 48;
|
|
308
|
+
console.log(` ${padVisible('Base score', breakdownWidth - 6)} ${white('100')}`);
|
|
309
|
+
|
|
310
|
+
if (stats.critical > 0) {
|
|
311
|
+
const critDed = stats.critical * 30;
|
|
312
|
+
console.log(` ${padVisible(`${stats.critical} × Critical finding${stats.critical > 1 ? 's' : ''}`, breakdownWidth - 6)} ${red(`- ${critDed}`)}`);
|
|
313
|
+
}
|
|
314
|
+
if (stats.high > 0) {
|
|
315
|
+
const highDed = stats.high * 10;
|
|
316
|
+
console.log(` ${padVisible(`${stats.high} × High finding${stats.high > 1 ? 's' : ''}`, breakdownWidth - 6)} ${orange(`- ${highDed}`)}`);
|
|
317
|
+
}
|
|
318
|
+
if (stats.medium > 0) {
|
|
319
|
+
const medDed = stats.medium * 3;
|
|
320
|
+
console.log(` ${padVisible(`${stats.medium} × Medium finding${stats.medium > 1 ? 's' : ''}`, breakdownWidth - 6)} ${yellow(`- ${medDed}`)}`);
|
|
321
|
+
}
|
|
322
|
+
if (stats.low > 0) {
|
|
323
|
+
const lowDed = stats.low * 1;
|
|
324
|
+
console.log(` ${padVisible(`${stats.low} × Low finding${stats.low > 1 ? 's' : ''}`, breakdownWidth - 6)} ${cyan(`- ${lowDed}`)}`);
|
|
325
|
+
}
|
|
326
|
+
if (stats.total === 0) {
|
|
327
|
+
console.log(` ${padVisible('No security deductions across assessed domains', breakdownWidth - 6)} ${green('+ 0')}`);
|
|
328
|
+
}
|
|
206
329
|
|
|
207
|
-
|
|
208
|
-
|
|
330
|
+
console.log(` ${darkBorder('─'.repeat(breakdownWidth))}`);
|
|
331
|
+
console.log(` ${padVisible('Final score', breakdownWidth - 6)} ${white.bold(String(scoreNum))}`);
|
|
332
|
+
console.log(` ${padVisible('Grade', breakdownWidth - 6)} ${gradeColor.bold(gradeLetter)}`);
|
|
333
|
+
|
|
334
|
+
if (stats.critical > 0) {
|
|
335
|
+
console.log(` ${red('Critical finding detected: Grade capped at F')}`);
|
|
336
|
+
}
|
|
337
|
+
console.log('');
|
|
338
|
+
|
|
339
|
+
// 5. Findings Table
|
|
340
|
+
console.log(cyan.bold('▶ FINDINGS'));
|
|
341
|
+
console.log('');
|
|
342
|
+
|
|
343
|
+
const displayLimit = 5;
|
|
344
|
+
const topFindings = findings.slice(0, displayLimit);
|
|
345
|
+
|
|
346
|
+
const colId = 15;
|
|
347
|
+
const colSev = 12;
|
|
348
|
+
const colIssue = 36;
|
|
349
|
+
const colLoc = 24;
|
|
350
|
+
|
|
351
|
+
const headerRow = ` ${dimGray(padVisible('ID', colId))} ${dimGray(padVisible('SEVERITY', colSev))} ${dimGray(padVisible('ISSUE', colIssue))} ${dimGray('LOCATION')}`;
|
|
352
|
+
console.log(headerRow);
|
|
353
|
+
console.log(` ${darkBorder('─'.repeat(colId + colSev + colIssue + colLoc))}`);
|
|
354
|
+
|
|
355
|
+
if (findings.length === 0) {
|
|
356
|
+
console.log(` ${green('✓ No vulnerabilities detected in scanned domains.')}`);
|
|
209
357
|
} else {
|
|
210
|
-
|
|
211
|
-
|
|
358
|
+
findings.forEach((f, idx) => {
|
|
359
|
+
if (idx >= displayLimit) return;
|
|
360
|
+
// Consistent ID format VG-FIND-001
|
|
361
|
+
const id = f.id || `VG-FIND-${String(idx + 1).padStart(3, '0')}`;
|
|
212
362
|
const sev = (f.severity || 'LOW').toUpperCase();
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
else if (sev === '
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
const
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
const
|
|
223
|
-
const
|
|
224
|
-
|
|
363
|
+
|
|
364
|
+
let sevFormatted = cyan('LOW ');
|
|
365
|
+
if (sev === 'CRITICAL') sevFormatted = red.bold('CRITICAL');
|
|
366
|
+
else if (sev === 'HIGH') sevFormatted = orange.bold('HIGH ');
|
|
367
|
+
else if (sev === 'MEDIUM') sevFormatted = yellow('MEDIUM ');
|
|
368
|
+
|
|
369
|
+
const rawTitle = maskSecrets(f.title || 'Security Finding');
|
|
370
|
+
const truncatedTitle = rawTitle.length > 33 ? rawTitle.slice(0, 31) + '..' : rawTitle;
|
|
371
|
+
|
|
372
|
+
const loc = `${f.file || 'repo'}:${f.line || 1}`;
|
|
373
|
+
const row = ` ${white(padVisible(id, colId))} ${padVisible(sevFormatted, colSev)} ${white(padVisible(truncatedTitle, colIssue))} ${dimGray(loc)}`;
|
|
374
|
+
console.log(row);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
if (findings.length > displayLimit) {
|
|
378
|
+
console.log(`\n ${dimGray(`Showing ${displayLimit} of ${findings.length} findings`)}`);
|
|
225
379
|
}
|
|
226
380
|
}
|
|
381
|
+
console.log('');
|
|
227
382
|
|
|
228
|
-
//
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
383
|
+
// 6. Structured AI Remediation Section (Directive 10 & 11)
|
|
384
|
+
if (remediation) {
|
|
385
|
+
if (remediation.status === 'UNAVAILABLE') {
|
|
386
|
+
console.log(cyan.bold('▶ AI REMEDIATION · UNAVAILABLE'));
|
|
387
|
+
console.log('');
|
|
388
|
+
console.log(gray(' Reason:'));
|
|
389
|
+
console.log(` ${yellow(remediation.unavailableReason || 'NVIDIA_API_KEY not configured.')}`);
|
|
390
|
+
console.log(` ${dimGray('Scanning, deterministic scoring, and policy enforcement remain 100% operational.')}\n`);
|
|
391
|
+
} else {
|
|
392
|
+
console.log(cyan.bold('▶ AI REMEDIATION · OPTIONAL'));
|
|
393
|
+
console.log('');
|
|
394
|
+
console.log(` ${gray('Finding:')} ${white.bold(remediation.findingId)}`);
|
|
395
|
+
if (remediation.severity) {
|
|
396
|
+
console.log(` ${gray('Severity:')} ${white(remediation.severity)}`);
|
|
397
|
+
}
|
|
398
|
+
console.log('');
|
|
399
|
+
|
|
400
|
+
console.log(` ${cyan('ISSUE')}`);
|
|
401
|
+
console.log(` ${white(maskSecrets(remediation.issue))}\n`);
|
|
402
|
+
|
|
403
|
+
if (remediation.impact) {
|
|
404
|
+
console.log(` ${cyan('IMPACT')}`);
|
|
405
|
+
console.log(` ${white(maskSecrets(remediation.impact))}\n`);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (remediation.recommendation) {
|
|
409
|
+
console.log(` ${cyan('RECOMMENDED FIX')}`);
|
|
410
|
+
console.log(` ${white(maskSecrets(remediation.recommendation))}\n`);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
console.log(` ${cyan('SUGGESTED FIX')}`);
|
|
414
|
+
if (remediation.hasConcretePatch && remediation.suggestedFix) {
|
|
415
|
+
console.log(` ${darkBorder('┌────────────────────────────────────────────────────────────┐')}`);
|
|
416
|
+
const lines = maskSecrets(remediation.suggestedFix).split('\n');
|
|
417
|
+
for (const line of lines) {
|
|
418
|
+
let colored = white(line);
|
|
419
|
+
if (line.trim().startsWith('+')) colored = green(line);
|
|
420
|
+
else if (line.trim().startsWith('-')) colored = red(line);
|
|
421
|
+
else if (line.trim().startsWith('#') || line.trim().startsWith('//')) colored = dimGray(line);
|
|
422
|
+
console.log(` ${darkBorder('│')} ${padVisible(colored, 58)} ${darkBorder('│')}`);
|
|
423
|
+
}
|
|
424
|
+
console.log(` ${darkBorder('└────────────────────────────────────────────────────────────┘')}`);
|
|
425
|
+
|
|
426
|
+
if (remediation.confidence) {
|
|
427
|
+
console.log(`\n ${gray('Confidence:')} ${green.bold(remediation.confidence + '%')}`);
|
|
428
|
+
}
|
|
429
|
+
console.log(` ${gray('Status:')} ${white.bold(remediation.status)}`);
|
|
430
|
+
} else {
|
|
431
|
+
console.log(` ${yellow('No patch generated — guidance only.')}\n`);
|
|
432
|
+
console.log(` ${gray('Status:')} ${yellow.bold('GUIDANCE ONLY')}`);
|
|
433
|
+
}
|
|
434
|
+
console.log('');
|
|
435
|
+
}
|
|
238
436
|
}
|
|
239
437
|
|
|
240
|
-
//
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
console.log(
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
438
|
+
// 7. Repository Metadata (Directive 13)
|
|
439
|
+
console.log(cyan.bold('▶ REPOSITORY'));
|
|
440
|
+
console.log('');
|
|
441
|
+
console.log(` ${gray('Name'.padEnd(12))} ${white(gitInfo.name)}`);
|
|
442
|
+
console.log(` ${gray('Branch'.padEnd(12))} ${white(gitInfo.branch)}`);
|
|
443
|
+
console.log(` ${gray('Commit'.padEnd(12))} ${white(gitInfo.commit)}`);
|
|
444
|
+
console.log(` ${gray('Duration'.padEnd(12))} ${white(duration)}`);
|
|
445
|
+
if (policyThreshold) {
|
|
446
|
+
console.log(` ${gray('Policy'.padEnd(12))} ${white('fail-on ' + policyThreshold)}`);
|
|
447
|
+
}
|
|
448
|
+
console.log('');
|
|
449
|
+
|
|
450
|
+
// 8. Truthful Summary (Directive 14)
|
|
451
|
+
console.log(cyan.bold('▶ SUMMARY'));
|
|
452
|
+
console.log('');
|
|
453
|
+
const evaluatedCount = scanners.length;
|
|
454
|
+
const executedCount = scanners.filter(s => String(s.state).toUpperCase() === 'SUCCESS').length;
|
|
455
|
+
|
|
456
|
+
console.log(` ${green('✓')} Scan completed in ${duration}`);
|
|
457
|
+
console.log(` ${green('✓')} ${evaluatedCount} scanners evaluated`);
|
|
458
|
+
console.log(` ${green('✓')} ${executedCount} scanner${executedCount === 1 ? '' : 's'} executed`);
|
|
459
|
+
console.log(` ${green('✓')} ${stats.total} findings detected (${stats.critical} critical, ${stats.high} high, ${stats.medium} medium, ${stats.low} low)`);
|
|
460
|
+
console.log('');
|
|
461
|
+
|
|
462
|
+
// 9. Local vs Cloud Synchronization Status (Directive 15)
|
|
463
|
+
console.log(` ${green('✓')} Local scan completed`);
|
|
464
|
+
if (syncStatus === 'SYNCED') {
|
|
465
|
+
console.log(` ${green('✓')} Results synced to VibeGuard Cloud (Tenant Isolated)`);
|
|
466
|
+
} else if (syncStatus === 'FAILED') {
|
|
467
|
+
console.log(` ${red('✗')} Cloud sync failed (local result preserved)`);
|
|
468
|
+
} else {
|
|
469
|
+
console.log(` ${dimGray('○')} Cloud sync skipped — run 'vibeguard scan --sync' to stream telemetry`);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// 10. Privacy Guarantee (Directive 16)
|
|
473
|
+
console.log(`\n ${dimGray('Privacy: Source code remains local. Only normalized security metadata is synchronized.')}\n`);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
export function renderCIOutput(options: {
|
|
477
|
+
deterministicScore: DeterministicScore;
|
|
478
|
+
findings: NormalizedFinding[];
|
|
479
|
+
policyPassed: boolean;
|
|
480
|
+
failThreshold: string;
|
|
481
|
+
scanners?: ScannerTelemetry[];
|
|
482
|
+
verbose?: boolean;
|
|
483
|
+
}): string[] {
|
|
484
|
+
const lines: string[] = [];
|
|
485
|
+
const breakdown = options.deterministicScore.breakdown || calculateScore(options.findings);
|
|
486
|
+
lines.push('VibeGuard Security Policy');
|
|
487
|
+
lines.push('');
|
|
488
|
+
lines.push(`Score: ${options.deterministicScore.score}/100 (${options.deterministicScore.grade})`);
|
|
489
|
+
lines.push(`Findings: ${options.findings.length}`);
|
|
490
|
+
lines.push(`Critical: ${breakdown.critical}`);
|
|
491
|
+
lines.push(`High: ${breakdown.high}`);
|
|
492
|
+
lines.push(`Medium: ${breakdown.medium}`);
|
|
493
|
+
lines.push(`Low: ${breakdown.low}`);
|
|
494
|
+
lines.push('');
|
|
495
|
+
lines.push(`Policy: ${options.policyPassed ? 'PASS' : 'FAIL'}`);
|
|
496
|
+
lines.push(`Threshold: ${options.failThreshold.toUpperCase()}`);
|
|
497
|
+
|
|
498
|
+
if (options.verbose && options.scanners) {
|
|
499
|
+
lines.push('');
|
|
500
|
+
lines.push('Scanner Telemetry:');
|
|
501
|
+
for (const sr of options.scanners) {
|
|
502
|
+
lines.push(` - ${sr.scanner.padEnd(14)}: ${sr.state.padEnd(14)} (${sr.durationMs || 0}ms) findings: ${sr.findingsCount || 0}`);
|
|
260
503
|
}
|
|
261
|
-
|
|
504
|
+
}
|
|
505
|
+
return lines;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
export function generateJsonOutput(options: {
|
|
509
|
+
deterministicScore: DeterministicScore;
|
|
510
|
+
findings: NormalizedFinding[];
|
|
511
|
+
coverageData: ScannerCoverage;
|
|
512
|
+
activeDomainsCount: number;
|
|
513
|
+
postureStatus: 'COMPLETE' | 'PARTIAL';
|
|
514
|
+
scanners: ScannerTelemetry[];
|
|
515
|
+
gitInfo: { name: string; branch: string; commit: string };
|
|
516
|
+
policyPassed: boolean;
|
|
517
|
+
failThreshold: string;
|
|
518
|
+
durationMs: number;
|
|
519
|
+
}) {
|
|
520
|
+
return {
|
|
521
|
+
score: options.deterministicScore.score,
|
|
522
|
+
grade: options.deterministicScore.grade,
|
|
523
|
+
postureStatus: options.postureStatus,
|
|
524
|
+
coverage: {
|
|
525
|
+
assessedDomains: options.activeDomainsCount,
|
|
526
|
+
totalDomains: 7,
|
|
527
|
+
domains: options.coverageData
|
|
528
|
+
},
|
|
529
|
+
deductions: options.deterministicScore.deductions,
|
|
530
|
+
breakdown: options.deterministicScore.breakdown || calculateScore(options.findings),
|
|
531
|
+
explanation: options.deterministicScore.explanation,
|
|
532
|
+
findings: options.findings.map(f => ({
|
|
533
|
+
id: f.id,
|
|
534
|
+
title: maskSecrets(f.title || ''),
|
|
535
|
+
severity: f.severity,
|
|
536
|
+
scanner: f.scanner,
|
|
537
|
+
file: f.file,
|
|
538
|
+
line: f.line,
|
|
539
|
+
ruleId: f.ruleId,
|
|
540
|
+
cwe: f.cwe,
|
|
541
|
+
owasp: f.owasp
|
|
542
|
+
})),
|
|
543
|
+
scanners: options.scanners,
|
|
544
|
+
repository: {
|
|
545
|
+
name: options.gitInfo.name,
|
|
546
|
+
branch: options.gitInfo.branch,
|
|
547
|
+
commit: options.gitInfo.commit
|
|
548
|
+
},
|
|
549
|
+
policyResult: {
|
|
550
|
+
passed: options.policyPassed,
|
|
551
|
+
threshold: options.failThreshold.toUpperCase()
|
|
552
|
+
},
|
|
553
|
+
durationMs: options.durationMs
|
|
554
|
+
};
|
|
555
|
+
}
|
|
262
556
|
|
|
263
|
-
|
|
557
|
+
export function evaluatePolicy(
|
|
558
|
+
failThreshold: string,
|
|
559
|
+
breakdown: { critical: number; high: number; medium: number; low: number },
|
|
560
|
+
totalFindings: number
|
|
561
|
+
): { passed: boolean; exitCode: number } {
|
|
562
|
+
const normThreshold = (failThreshold || 'high').toLowerCase();
|
|
563
|
+
let thresholdBreached = false;
|
|
564
|
+
|
|
565
|
+
if (normThreshold === 'critical') {
|
|
566
|
+
thresholdBreached = breakdown.critical > 0;
|
|
567
|
+
} else if (normThreshold === 'high') {
|
|
568
|
+
thresholdBreached = breakdown.critical > 0 || breakdown.high > 0;
|
|
569
|
+
} else if (normThreshold === 'medium') {
|
|
570
|
+
thresholdBreached = breakdown.critical > 0 || breakdown.high > 0 || breakdown.medium > 0;
|
|
571
|
+
} else if (normThreshold === 'low') {
|
|
572
|
+
thresholdBreached = totalFindings > 0;
|
|
264
573
|
}
|
|
265
574
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
const barMid = `│ ${summaryText} │`;
|
|
271
|
-
const barBot = `└${'─'.repeat(visibleWidth(summaryText) + 4)}┘`;
|
|
272
|
-
console.log(cyan(barTop));
|
|
273
|
-
console.log(cyan(barMid));
|
|
274
|
-
console.log(cyan(barBot));
|
|
275
|
-
console.log(`\n💡 ${gray('Tip: Run')} ${cyan('`vibeguard watch`')} ${gray('to continuously monitor your codebase.')}\n`);
|
|
575
|
+
return {
|
|
576
|
+
passed: !thresholdBreached,
|
|
577
|
+
exitCode: thresholdBreached ? 1 : 0
|
|
578
|
+
};
|
|
276
579
|
}
|