@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/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; // ignore variation selectors
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 = '4f2c1ab';
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, policy: 'enterprise' };
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
- // Calculate risk score: 100 is best, 0 is worst
86
- const deductions = (critical * 15) + (high * 7) + (medium * 3) + (low * 1);
87
- const score = Math.max(10, Math.min(100, 100 - deductions));
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 (score < 50 || critical > 0) riskLevel = 'CRITICAL RISK';
91
- else if (score < 75 || high > 0) riskLevel = 'HIGH RISK';
92
- else if (score < 90 || medium > 0) riskLevel = 'MEDIUM RISK';
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
- findings: NormalizedFinding[];
107
- stats: ScanStats;
108
- gitInfo: ReturnType<typeof getGitInfo>;
109
- duration: string;
110
- remediation?: {
111
- findingId: string;
112
- issue: string;
113
- impact: string;
114
- recommendation: string;
115
- diffSnippet: string;
116
- confidence: number;
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
- const now = new Date();
132
- const timeStr = now.toTimeString().split(' ')[0];
133
-
134
- // Shield ASCII Art
135
- const shield = [
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 (VIBEGUARD)
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
- // Print Top Row: Clock aligned right
159
- console.log(' '.repeat(65) + cyan.bold(timeStr));
160
-
161
- // 1. Header (Shield + VIBEGUARD)
162
- console.log(`${cyan(shield[0])} ${cyan.bold(title[0])}`);
163
- console.log(`${cyan(shield[1])} ${cyan.bold(title[1])}`);
164
- console.log(`${cyan(shield[2])} ${cyan.bold(title[2])}`);
165
- console.log(`${cyan(shield[3])} ${cyan.bold(title[3])}`);
166
- console.log(`${cyan(shield[4])} ${cyan.bold(title[4])}`);
167
- console.log(`${cyan(shield[5])} ${gray('AI-Powered DevSecOps Orchestrator')}`);
168
- console.log(` ${cyan('Scanning. Analyzing. Protecting.')}\n`);
169
-
170
- // 2. Risk Score Box (Placed cleanly BELOW VIBEGUARD Header)
171
- const boxWidth = 74;
172
- console.log(darkBorder(`┌${''.repeat(boxWidth)}┐`));
173
- console.log(`${darkBorder('│')} ${cyan.bold('OVERALL RISK SCORE')}${' '.repeat(boxWidth - 20)}${darkBorder('│')}`);
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
- console.log(`${darkBorder('│')}${padVisible(scoreLine1, boxWidth)}${darkBorder('')}`);
180
- console.log(`${darkBorder('│')}${padVisible(scoreLine2, boxWidth)}${darkBorder('')}`);
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. Multi-panel Grid (Pipeline & Findings)
185
- const pipelineRows = [
186
- `</> SAST (Semgrep) ${green('✓ OK')}`,
187
- `📦 Dependency Check (Trivy) ${green('✓ OK')}`,
188
- `🔑 Secrets Scan (Gitleaks) ${green('✓ OK')}`,
189
- `🔒 IaC Scan (Checkov) ${green('✓ OK')}`,
190
- `🐳 Container Scan (Trivy) ${green('✓ OK')}`,
191
- `📑 Code Quality (ESLint) ${green('✓ OK')}`,
192
- ``,
193
- `${cyan('▶ REPOSITORY')}`,
194
- `${gray('Name:')} ${white(gitInfo.name)}`,
195
- `${gray('Branch:')} ${white(gitInfo.branch)}`,
196
- `${gray('Commit:')} ${white(gitInfo.commit)}`,
197
- `${gray('Scan Time:')} ${white(duration)}`,
198
- `${gray('Policy:')} ${white(gitInfo.policy)}`
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
- // Table of findings (top 10)
202
- const topFindings = findings.slice(0, 10);
203
- const tableHeader = `${dimGray(padVisible('ID', 13))} ${dimGray(padVisible('SEVERITY', 10))} ${dimGray(padVisible('TITLE', 28))} ${dimGray('FILE:LINE')}`;
204
-
205
- const findingRows: string[] = [tableHeader];
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
- if (topFindings.length === 0) {
208
- findingRows.push(green(' No security vulnerabilities detected. Codebase is clean!'));
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
- for (const f of topFindings) {
211
- const id = f.id || 'VG-FIND';
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
- let sevFormatted = cyan('LOW ');
214
- if (sev === 'CRITICAL') sevFormatted = red.bold('CRITICAL ');
215
- else if (sev === 'HIGH') sevFormatted = orange.bold('HIGH ');
216
- else if (sev === 'MEDIUM') sevFormatted = yellow('MEDIUM ');
217
-
218
- const rawTitle = f.title || 'Security Finding';
219
- const titleTruncated = rawTitle.length > 26 ? rawTitle.slice(0, 24) + '..' : rawTitle;
220
- const titleFormatted = padVisible(white(titleTruncated), 28);
221
-
222
- const fileLine = `${f.file || 'unknown'}:${f.line || 1}`;
223
- const idFormatted = padVisible(cyan(id), 13);
224
- findingRows.push(`${idFormatted} ${sevFormatted} ${titleFormatted} ${dimGray(fileLine)}`);
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
- // Print Section Headers with ANSI-safe padding
229
- const headerCol1 = padVisible(cyan('▶ SCANNER PIPELINE'), 36);
230
- const headerCol2 = cyan('▶ TOP FINDINGS');
231
- console.log(`${headerCol1} ${headerCol2}`);
232
-
233
- const maxRows = Math.max(pipelineRows.length, findingRows.length);
234
- for (let i = 0; i < maxRows; i++) {
235
- const left = padVisible(pipelineRows[i] || '', 36);
236
- const right = findingRows[i] || '';
237
- console.log(`${left} ${right}`);
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
- // 4. AI Remediation Section
241
- if (remediation) {
242
- console.log(`\n${cyan('▶ AI REMEDIATION (VG-AI)')}\n`);
243
- console.log(`${cyan('Finding:')} ${red.bold(remediation.findingId)}\n`);
244
- console.log(`${cyan('Issue')}\n${white(remediation.issue)}\n`);
245
- console.log(`${cyan('Impact')}\n${white(remediation.impact)}\n`);
246
- console.log(`${cyan('Recommendation')}\n${white(remediation.recommendation)}\n`);
247
-
248
- console.log(`${cyan('Suggested Fix')}`);
249
- console.log(darkBorder('┌────────────────────────────────────────────────────────────┐'));
250
- const diffLines = remediation.diffSnippet.split('\n');
251
- for (const d of diffLines) {
252
- let styled = d;
253
- if (d.trim().startsWith('-')) styled = red(d);
254
- else if (d.trim().startsWith('+')) styled = green(d);
255
- else if (d.trim().startsWith('#')) styled = dimGray(d);
256
- else styled = white(d);
257
-
258
- const paddedLine = padVisible(styled, 58);
259
- console.log(`${darkBorder('')} ${paddedLine} ${darkBorder('│')}`);
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
- console.log(darkBorder('└────────────────────────────────────────────────────────────┘'));
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
- console.log(`\n${cyan('Confidence:')} ${green.bold(remediation.confidence + '%')}`);
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
- // 5. Summary Bar (bottom capsule)
267
- console.log(`\n${cyan('▶ SUMMARY')}`);
268
- const summaryText = `🛡️ Scan completed in ${duration} │ ${stats.total} findings │ 6 passed`;
269
- const barTop = `┌${'─'.repeat(visibleWidth(summaryText) + 4)}┐`;
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
  }