@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.
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getCredentialsDir = getCredentialsDir;
7
+ exports.getCredentialsPath = getCredentialsPath;
8
+ exports.saveCredentials = saveCredentials;
9
+ exports.loadCredentials = loadCredentials;
10
+ exports.clearCredentials = clearCredentials;
11
+ const fs_1 = __importDefault(require("fs"));
12
+ const path_1 = __importDefault(require("path"));
13
+ const os_1 = __importDefault(require("os"));
14
+ function getCredentialsDir() {
15
+ return path_1.default.join(os_1.default.homedir(), '.vibeguard');
16
+ }
17
+ function getCredentialsPath() {
18
+ return path_1.default.join(getCredentialsDir(), 'credentials.json');
19
+ }
20
+ function saveCredentials(creds) {
21
+ const dir = getCredentialsDir();
22
+ if (!fs_1.default.existsSync(dir)) {
23
+ fs_1.default.mkdirSync(dir, { recursive: true });
24
+ }
25
+ const credsPath = getCredentialsPath();
26
+ const payload = {
27
+ token: creds.token,
28
+ user: creds.user,
29
+ apiUrl: creds.apiUrl || process.env.VIBEGUARD_API_URL || 'https://vibeguard-eep3.onrender.com',
30
+ savedAt: new Date().toISOString()
31
+ };
32
+ fs_1.default.writeFileSync(credsPath, JSON.stringify(payload, null, 2), { encoding: 'utf-8', mode: 0o600 });
33
+ }
34
+ function loadCredentials() {
35
+ const credsPath = getCredentialsPath();
36
+ if (!fs_1.default.existsSync(credsPath)) {
37
+ return null;
38
+ }
39
+ try {
40
+ const raw = fs_1.default.readFileSync(credsPath, 'utf-8');
41
+ const parsed = JSON.parse(raw);
42
+ if (!parsed.token || !parsed.user?.email) {
43
+ return null;
44
+ }
45
+ return parsed;
46
+ }
47
+ catch {
48
+ return null;
49
+ }
50
+ }
51
+ function clearCredentials() {
52
+ const credsPath = getCredentialsPath();
53
+ if (fs_1.default.existsSync(credsPath)) {
54
+ try {
55
+ fs_1.default.unlinkSync(credsPath);
56
+ return true;
57
+ }
58
+ catch {
59
+ return false;
60
+ }
61
+ }
62
+ return true;
63
+ }
package/dist/formatter.js CHANGED
@@ -3,13 +3,28 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.maskSecrets = maskSecrets;
6
7
  exports.getGitInfo = getGitInfo;
7
8
  exports.calculateScore = calculateScore;
8
9
  exports.renderDashboard = renderDashboard;
10
+ exports.renderCIOutput = renderCIOutput;
11
+ exports.generateJsonOutput = generateJsonOutput;
12
+ exports.evaluatePolicy = evaluatePolicy;
9
13
  const chalk_1 = __importDefault(require("chalk"));
10
14
  const child_process_1 = require("child_process");
11
15
  const types_1 = require("@maverick006/types");
12
16
  const path_1 = __importDefault(require("path"));
17
+ function maskSecrets(input) {
18
+ if (!input)
19
+ return input;
20
+ let masked = input;
21
+ masked = masked.replace(/\b(AKIA[0-9A-Z]{16})\b/g, 'AKIA****************');
22
+ masked = masked.replace(/\b(gh[pousr]_[A-Za-z0-9_]{36,255})\b/g, 'ghp_************************************');
23
+ masked = masked.replace(/(bearer\s+)([a-zA-Z0-9_\-\.]{15,})/gi, '$1[REDACTED]');
24
+ masked = masked.replace(/(password|secret|api[_-]?key)\s*[:=]\s*['"]?([a-zA-Z0-9_\-\.]{8,})['"]?/gi, '$1: [REDACTED]');
25
+ masked = masked.replace(/-----BEGIN[ A-Z0-9_-]+PRIVATE KEY-----[\s\S]*?-----END[ A-Z0-9_-]+PRIVATE KEY-----/g, '[REDACTED_PRIVATE_KEY]');
26
+ return masked;
27
+ }
13
28
  function stripAnsi(str) {
14
29
  return str.replace(/\u001b\[[0-9;]*m/g, '');
15
30
  }
@@ -19,8 +34,7 @@ function visibleWidth(str) {
19
34
  for (const char of plain) {
20
35
  const code = char.codePointAt(0) || 0;
21
36
  if (code === 0xFE0F || code === 0xFE0E)
22
- continue; // ignore variation selectors
23
- // Emojis and certain symbols take 2 terminal columns
37
+ continue;
24
38
  if (code > 0x1F000 || (code >= 0x2600 && code <= 0x27BF)) {
25
39
  width += 2;
26
40
  }
@@ -39,7 +53,7 @@ function getGitInfo(cwd = process.cwd()) {
39
53
  const resolvedCwd = path_1.default.resolve(cwd);
40
54
  let name = path_1.default.basename(resolvedCwd);
41
55
  let branch = 'main';
42
- let commit = '4f2c1ab';
56
+ let commit = 'HEAD';
43
57
  let remoteUrl = '';
44
58
  try {
45
59
  const branchOut = (0, child_process_1.execSync)('git rev-parse --abbrev-ref HEAD', { cwd, stdio: ['pipe', 'pipe', 'ignore'] }).toString().trim();
@@ -63,13 +77,14 @@ function getGitInfo(cwd = process.cwd()) {
63
77
  }
64
78
  }
65
79
  catch { }
66
- return { name, branch, commit, remoteUrl, policy: 'enterprise' };
80
+ return { name, branch, commit, remoteUrl };
67
81
  }
68
82
  function calculateScore(findings) {
69
83
  let critical = 0;
70
84
  let high = 0;
71
85
  let medium = 0;
72
86
  let low = 0;
87
+ let info = 0;
73
88
  for (const f of findings) {
74
89
  const sev = (f.severity || '').toUpperCase();
75
90
  if (sev === 'CRITICAL' || sev === types_1.Severity.CRITICAL)
@@ -78,31 +93,49 @@ function calculateScore(findings) {
78
93
  high++;
79
94
  else if (sev === 'MEDIUM' || sev === types_1.Severity.MEDIUM)
80
95
  medium++;
81
- else
96
+ else if (sev === 'LOW' || sev === types_1.Severity.LOW)
82
97
  low++;
98
+ else
99
+ info++;
100
+ }
101
+ const deductions = (critical * 30) + (high * 10) + (medium * 3) + (low * 1);
102
+ let score = Math.max(0, Math.min(100, 100 - deductions));
103
+ let grade = 'A';
104
+ if (critical > 0) {
105
+ grade = 'F';
106
+ score = Math.min(score, 49);
83
107
  }
84
- // Calculate risk score: 100 is best, 0 is worst
85
- const deductions = (critical * 15) + (high * 7) + (medium * 3) + (low * 1);
86
- const score = Math.max(10, Math.min(100, 100 - deductions));
108
+ else if (score >= 90)
109
+ grade = 'A';
110
+ else if (score >= 80)
111
+ grade = 'B';
112
+ else if (score >= 70)
113
+ grade = 'C';
114
+ else if (score >= 50)
115
+ grade = 'D';
116
+ else
117
+ grade = 'F';
87
118
  let riskLevel = 'LOW RISK';
88
- if (score < 50 || critical > 0)
119
+ if (grade === 'F')
89
120
  riskLevel = 'CRITICAL RISK';
90
- else if (score < 75 || high > 0)
121
+ else if (grade === 'D')
91
122
  riskLevel = 'HIGH RISK';
92
- else if (score < 90 || medium > 0)
123
+ else if (grade === 'C' || grade === 'B')
93
124
  riskLevel = 'MEDIUM RISK';
94
125
  return {
95
126
  critical,
96
127
  high,
97
128
  medium,
98
129
  low,
130
+ info,
99
131
  total: findings.length,
100
132
  score,
133
+ grade,
101
134
  riskLevel
102
135
  };
103
136
  }
104
137
  function renderDashboard(options) {
105
- const { findings, stats, gitInfo, duration, remediation } = options;
138
+ const { findings, stats, deterministicScore, coverage, gitInfo, duration, scanners, remediation, syncStatus, policyThreshold, verbose } = options;
106
139
  const cyan = chalk_1.default.hex('#00E5FF');
107
140
  const gray = chalk_1.default.hex('#94A3B8');
108
141
  const dimGray = chalk_1.default.hex('#475569');
@@ -112,18 +145,19 @@ function renderDashboard(options) {
112
145
  const orange = chalk_1.default.hex('#F97316');
113
146
  const yellow = chalk_1.default.hex('#F59E0B');
114
147
  const white = chalk_1.default.white;
115
- const now = new Date();
116
- const timeStr = now.toTimeString().split(' ')[0];
117
- // Shield ASCII Art
118
- const shield = [
119
- ' ,-----. ',
120
- ' / _ \\ ',
121
- ' | / \\ | ',
122
- ' | | ✓ | | ',
123
- ' \\ \\ / / ',
124
- ' `-------\' '
148
+ // Active domains count (out of 7)
149
+ const domainKeys = [
150
+ 'code',
151
+ 'dependencies',
152
+ 'secrets',
153
+ 'containers',
154
+ 'iac',
155
+ 'web',
156
+ 'cloud'
125
157
  ];
126
- // Title ASCII Art (VIBEGUARD)
158
+ const activeDomainsCount = domainKeys.filter(k => coverage[k]).length;
159
+ const isPartial = activeDomainsCount < 7;
160
+ // Title ASCII Art
127
161
  const title = [
128
162
  ' __ __ ___ ____ _____ ____ _ _ _ ____ ____ ',
129
163
  '\\ \\ / /|_ _| __ )| ____/ ___| | | | / \\ | _ \\| _ \\ ',
@@ -131,116 +165,326 @@ function renderDashboard(options) {
131
165
  ' \\ V / | || |_) | |___| |_| |_| / ___ \\ _ <| |_| |',
132
166
  ' \\_/ |___|____/|_____|\\____|\\___/_/ \\_\\_| \\_\\____/'
133
167
  ];
134
- const scoreColor = stats.score >= 85 ? green : stats.score >= 65 ? yellow : red;
135
- const riskColor = stats.riskLevel === 'LOW RISK' ? green : stats.riskLevel === 'MEDIUM RISK' ? yellow : red;
136
168
  console.log('\n');
137
- // Print Top Row: Clock aligned right
138
- console.log(' '.repeat(65) + cyan.bold(timeStr));
139
- // 1. Header (Shield + VIBEGUARD)
140
- console.log(`${cyan(shield[0])} ${cyan.bold(title[0])}`);
141
- console.log(`${cyan(shield[1])} ${cyan.bold(title[1])}`);
142
- console.log(`${cyan(shield[2])} ${cyan.bold(title[2])}`);
143
- console.log(`${cyan(shield[3])} ${cyan.bold(title[3])}`);
144
- console.log(`${cyan(shield[4])} ${cyan.bold(title[4])}`);
145
- console.log(`${cyan(shield[5])} ${gray('AI-Powered DevSecOps Orchestrator')}`);
146
- console.log(` ${cyan('Scanning. Analyzing. Protecting.')}\n`);
147
- // 2. Risk Score Box (Placed cleanly BELOW VIBEGUARD Header)
148
- const boxWidth = 74;
149
- console.log(darkBorder(`┌${''.repeat(boxWidth)}┐`));
150
- console.log(`${darkBorder('│')} ${cyan.bold('OVERALL RISK SCORE')}${' '.repeat(boxWidth - 20)}${darkBorder('│')}`);
169
+ // 1. Header (VIBEGUARD - Cloud + Security Posture)
170
+ for (const line of title) {
171
+ console.log(cyan.bold(line));
172
+ }
173
+ console.log(`\n${white.bold('VIBEGUARD')} ${dimGray('│')} ${cyan('Cloud + Security Posture')}`);
174
+ console.log(gray('Scanning. Analyzing. Protecting.\n'));
175
+ // 2. Redesigned Security Posture Header (Honest Partial Posture Representation)
176
+ const boxWidth = 76;
177
+ console.log(darkBorder(`┌─ SECURITY POSTURE ${'─'.repeat(boxWidth - 21)}┐`));
178
+ console.log(`${darkBorder('│')}${' '.repeat(boxWidth)}${darkBorder('│')}`);
179
+ const scoreNum = deterministicScore?.score ?? stats.score;
180
+ const gradeLetter = deterministicScore?.grade ?? stats.grade;
181
+ const gradeColor = gradeLetter === 'A' ? green : gradeLetter === 'B' ? cyan : gradeLetter === 'C' ? yellow : red;
182
+ const scoreLine = ` ${gradeColor.bold(String(scoreNum))} ${gray('/ 100')} ${gradeColor.bold('Grade ' + gradeLetter)}`;
183
+ console.log(`${darkBorder('│')}${padVisible(scoreLine, boxWidth)}${darkBorder('│')}`);
184
+ const postureBadge = isPartial
185
+ ? yellow.bold('PARTIAL POSTURE') + dimGray(` (${activeDomainsCount} / 7 security domains assessed)`)
186
+ : green.bold('COMPLETE POSTURE') + dimGray(' (7 / 7 security domains assessed)');
187
+ const statusLine = ` ${postureBadge}`;
188
+ console.log(`${darkBorder('│')}${padVisible(statusLine, boxWidth)}${darkBorder('│')}`);
151
189
  console.log(`${darkBorder('│')}${' '.repeat(boxWidth)}${darkBorder('│')}`);
152
- const scoreLine1 = ` ${scoreColor.bold(String(stats.score))} ${gray('/100')} ${red('CRITICAL')} ${String(stats.critical).padEnd(2, ' ')} ${orange('HIGH')} ${String(stats.high).padEnd(2, ' ')}`;
153
- const scoreLine2 = ` ${riskColor.bold(stats.riskLevel.padEnd(13, ' '))} ${yellow('MEDIUM')} ${String(stats.medium).padEnd(2, ' ')} ${cyan('LOW')} ${String(stats.low).padEnd(2, ' ')}`;
154
- console.log(`${darkBorder('│')}${padVisible(scoreLine1, boxWidth)}${darkBorder('')}`);
155
- console.log(`${darkBorder('│')}${padVisible(scoreLine2, boxWidth)}${darkBorder('')}`);
190
+ const critStr = stats.critical > 0 ? red.bold(`${stats.critical} Critical`) : dimGray('0 Critical');
191
+ const highStr = stats.high > 0 ? orange.bold(`${stats.high} High`) : dimGray('0 High');
192
+ const medStr = stats.medium > 0 ? yellow.bold(`${stats.medium} Medium`) : dimGray('0 Medium');
193
+ const lowStr = stats.low > 0 ? cyan(`${stats.low} Low`) : dimGray('0 Low');
194
+ const findingsLine = ` ${critStr} ${highStr} ${medStr} ${lowStr} ${gray(`(${stats.total} total ${stats.total === 1 ? 'finding' : 'findings'})`)}`;
195
+ console.log(`${darkBorder('│')}${padVisible(findingsLine, boxWidth)}${darkBorder('│')}`);
196
+ const coverageLine = ` Coverage: ${cyan(`${activeDomainsCount} / 7`)} security domains`;
197
+ console.log(`${darkBorder('│')}${padVisible(coverageLine, boxWidth)}${darkBorder('│')}`);
156
198
  console.log(darkBorder(`└${'─'.repeat(boxWidth)}┘`));
157
199
  console.log('');
158
- // 3. Multi-panel Grid (Pipeline & Findings)
159
- const pipelineRows = [
160
- `</> SAST (Semgrep) ${green('✓ OK')}`,
161
- `📦 Dependency Check (Trivy) ${green('✓ OK')}`,
162
- `🔑 Secrets Scan (Gitleaks) ${green('✓ OK')}`,
163
- `🔒 IaC Scan (Checkov) ${green('✓ OK')}`,
164
- `🐳 Container Scan (Trivy) ${green('✓ OK')}`,
165
- `📑 Code Quality (ESLint) ${green('✓ OK')}`,
166
- ``,
167
- `${cyan('▶ REPOSITORY')}`,
168
- `${gray('Name:')} ${white(gitInfo.name)}`,
169
- `${gray('Branch:')} ${white(gitInfo.branch)}`,
170
- `${gray('Commit:')} ${white(gitInfo.commit)}`,
171
- `${gray('Scan Time:')} ${white(duration)}`,
172
- `${gray('Policy:')} ${white(gitInfo.policy)}`
200
+ // 3. Truthful Scanner Status & Security Domains
201
+ console.log(cyan.bold('▶ SCANNER COVERAGE'));
202
+ console.log('');
203
+ for (const s of scanners) {
204
+ const dur = s.durationMs ? `${(s.durationMs / 1000).toFixed(1)}s` : '0.1s';
205
+ const name = s.scanner.padEnd(16);
206
+ let stateStr = '';
207
+ const normState = String(s.state).toUpperCase();
208
+ if (normState === 'SUCCESS') {
209
+ const count = s.findingsCount || 0;
210
+ const countLabel = `${count} ${count === 1 ? 'finding' : 'findings'}`;
211
+ stateStr = `${green('✓ SUCCESS')} ${white(countLabel.padEnd(14))} ${dimGray(dur)}`;
212
+ }
213
+ else if (normState === 'NOT_INSTALLED') {
214
+ stateStr = `${dimGray('○ NOT INSTALLED')}`;
215
+ }
216
+ else if (normState === 'SKIPPED') {
217
+ const r = (s.reason || '').toLowerCase();
218
+ if (r.includes('not applicable') || r.includes('no live web') || r.includes('no iac') || r.includes('credentials not configured')) {
219
+ stateStr = `${dimGray('— NOT APPLICABLE')} ${dimGray(s.reason ? `(${s.reason.replace(/^Skipped:\s*/i, '')})` : '')}`;
220
+ }
221
+ else {
222
+ stateStr = `${yellow('⚠ SKIPPED')} ${dimGray(s.reason || '')}`;
223
+ }
224
+ }
225
+ else if (normState === 'TIMEOUT') {
226
+ stateStr = `${red('⏱ TIMEOUT')} ${dimGray(`(${dur})`)}`;
227
+ }
228
+ else if (normState === 'FAILED') {
229
+ stateStr = `${red('✗ FAILED')} ${dimGray(s.reason || '')}`;
230
+ }
231
+ else {
232
+ stateStr = `${dimGray('— ' + normState)}`;
233
+ }
234
+ console.log(` ${name} ${stateStr}`);
235
+ }
236
+ // Domain mapping summary
237
+ console.log(`\n ${gray(`Domain Assessment: ${activeDomainsCount} / 7`)}`);
238
+ const domainLabels = [
239
+ { key: 'dependencies', label: 'Dependencies' },
240
+ { key: 'code', label: 'Code' },
241
+ { key: 'secrets', label: 'Secrets' },
242
+ { key: 'containers', label: 'Containers' },
243
+ { key: 'iac', label: 'IaC' },
244
+ { key: 'web', label: 'Web/API' },
245
+ { key: 'cloud', label: 'Cloud' }
173
246
  ];
174
- // Table of findings (top 10)
175
- const topFindings = findings.slice(0, 10);
176
- const tableHeader = `${dimGray(padVisible('ID', 13))} ${dimGray(padVisible('SEVERITY', 10))} ${dimGray(padVisible('TITLE', 28))} ${dimGray('FILE:LINE')}`;
177
- const findingRows = [tableHeader];
178
- if (topFindings.length === 0) {
179
- findingRows.push(green(' No security vulnerabilities detected. Codebase is clean!'));
247
+ const domainStr = domainLabels
248
+ .map(d => (coverage[d.key] ? green(`✓ ${d.label}`) : dimGray(`○ ${d.label}`)))
249
+ .join(' ');
250
+ console.log(` ${domainStr}\n`);
251
+ // 4. Score Breakdown
252
+ console.log(cyan.bold(' SCORE BREAKDOWN'));
253
+ console.log('');
254
+ const breakdownWidth = 48;
255
+ console.log(` ${padVisible('Base score', breakdownWidth - 6)} ${white('100')}`);
256
+ if (stats.critical > 0) {
257
+ const critDed = stats.critical * 30;
258
+ console.log(` ${padVisible(`${stats.critical} × Critical finding${stats.critical > 1 ? 's' : ''}`, breakdownWidth - 6)} ${red(`- ${critDed}`)}`);
259
+ }
260
+ if (stats.high > 0) {
261
+ const highDed = stats.high * 10;
262
+ console.log(` ${padVisible(`${stats.high} × High finding${stats.high > 1 ? 's' : ''}`, breakdownWidth - 6)} ${orange(`- ${highDed}`)}`);
263
+ }
264
+ if (stats.medium > 0) {
265
+ const medDed = stats.medium * 3;
266
+ console.log(` ${padVisible(`${stats.medium} × Medium finding${stats.medium > 1 ? 's' : ''}`, breakdownWidth - 6)} ${yellow(`- ${medDed}`)}`);
267
+ }
268
+ if (stats.low > 0) {
269
+ const lowDed = stats.low * 1;
270
+ console.log(` ${padVisible(`${stats.low} × Low finding${stats.low > 1 ? 's' : ''}`, breakdownWidth - 6)} ${cyan(`- ${lowDed}`)}`);
271
+ }
272
+ if (stats.total === 0) {
273
+ console.log(` ${padVisible('No security deductions across assessed domains', breakdownWidth - 6)} ${green('+ 0')}`);
274
+ }
275
+ console.log(` ${darkBorder('─'.repeat(breakdownWidth))}`);
276
+ console.log(` ${padVisible('Final score', breakdownWidth - 6)} ${white.bold(String(scoreNum))}`);
277
+ console.log(` ${padVisible('Grade', breakdownWidth - 6)} ${gradeColor.bold(gradeLetter)}`);
278
+ if (stats.critical > 0) {
279
+ console.log(` ${red('Critical finding detected: Grade capped at F')}`);
280
+ }
281
+ console.log('');
282
+ // 5. Findings Table
283
+ console.log(cyan.bold('▶ FINDINGS'));
284
+ console.log('');
285
+ const displayLimit = 5;
286
+ const topFindings = findings.slice(0, displayLimit);
287
+ const colId = 15;
288
+ const colSev = 12;
289
+ const colIssue = 36;
290
+ const colLoc = 24;
291
+ const headerRow = ` ${dimGray(padVisible('ID', colId))} ${dimGray(padVisible('SEVERITY', colSev))} ${dimGray(padVisible('ISSUE', colIssue))} ${dimGray('LOCATION')}`;
292
+ console.log(headerRow);
293
+ console.log(` ${darkBorder('─'.repeat(colId + colSev + colIssue + colLoc))}`);
294
+ if (findings.length === 0) {
295
+ console.log(` ${green('✓ No vulnerabilities detected in scanned domains.')}`);
180
296
  }
181
297
  else {
182
- for (const f of topFindings) {
183
- const id = f.id || 'VG-FIND';
298
+ findings.forEach((f, idx) => {
299
+ if (idx >= displayLimit)
300
+ return;
301
+ // Consistent ID format VG-FIND-001
302
+ const id = f.id || `VG-FIND-${String(idx + 1).padStart(3, '0')}`;
184
303
  const sev = (f.severity || 'LOW').toUpperCase();
185
- let sevFormatted = cyan('LOW ');
304
+ let sevFormatted = cyan('LOW ');
186
305
  if (sev === 'CRITICAL')
187
- sevFormatted = red.bold('CRITICAL ');
306
+ sevFormatted = red.bold('CRITICAL');
188
307
  else if (sev === 'HIGH')
189
- sevFormatted = orange.bold('HIGH ');
308
+ sevFormatted = orange.bold('HIGH ');
190
309
  else if (sev === 'MEDIUM')
191
- sevFormatted = yellow('MEDIUM ');
192
- const rawTitle = f.title || 'Security Finding';
193
- const titleTruncated = rawTitle.length > 26 ? rawTitle.slice(0, 24) + '..' : rawTitle;
194
- const titleFormatted = padVisible(white(titleTruncated), 28);
195
- const fileLine = `${f.file || 'unknown'}:${f.line || 1}`;
196
- const idFormatted = padVisible(cyan(id), 13);
197
- findingRows.push(`${idFormatted} ${sevFormatted} ${titleFormatted} ${dimGray(fileLine)}`);
310
+ sevFormatted = yellow('MEDIUM ');
311
+ const rawTitle = maskSecrets(f.title || 'Security Finding');
312
+ const truncatedTitle = rawTitle.length > 33 ? rawTitle.slice(0, 31) + '..' : rawTitle;
313
+ const loc = `${f.file || 'repo'}:${f.line || 1}`;
314
+ const row = ` ${white(padVisible(id, colId))} ${padVisible(sevFormatted, colSev)} ${white(padVisible(truncatedTitle, colIssue))} ${dimGray(loc)}`;
315
+ console.log(row);
316
+ });
317
+ if (findings.length > displayLimit) {
318
+ console.log(`\n ${dimGray(`Showing ${displayLimit} of ${findings.length} findings`)}`);
198
319
  }
199
320
  }
200
- // Print Section Headers with ANSI-safe padding
201
- const headerCol1 = padVisible(cyan('▶ SCANNER PIPELINE'), 36);
202
- const headerCol2 = cyan('▶ TOP FINDINGS');
203
- console.log(`${headerCol1} ${headerCol2}`);
204
- const maxRows = Math.max(pipelineRows.length, findingRows.length);
205
- for (let i = 0; i < maxRows; i++) {
206
- const left = padVisible(pipelineRows[i] || '', 36);
207
- const right = findingRows[i] || '';
208
- console.log(`${left} ${right}`);
209
- }
210
- // 4. AI Remediation Section
321
+ console.log('');
322
+ // 6. Structured AI Remediation Section (Directive 10 & 11)
211
323
  if (remediation) {
212
- console.log(`\n${cyan('▶ AI REMEDIATION (VG-AI)')}\n`);
213
- console.log(`${cyan('Finding:')} ${red.bold(remediation.findingId)}\n`);
214
- console.log(`${cyan('Issue')}\n${white(remediation.issue)}\n`);
215
- console.log(`${cyan('Impact')}\n${white(remediation.impact)}\n`);
216
- console.log(`${cyan('Recommendation')}\n${white(remediation.recommendation)}\n`);
217
- console.log(`${cyan('Suggested Fix')}`);
218
- console.log(darkBorder('┌────────────────────────────────────────────────────────────┐'));
219
- const diffLines = remediation.diffSnippet.split('\n');
220
- for (const d of diffLines) {
221
- let styled = d;
222
- if (d.trim().startsWith('-'))
223
- styled = red(d);
224
- else if (d.trim().startsWith('+'))
225
- styled = green(d);
226
- else if (d.trim().startsWith('#'))
227
- styled = dimGray(d);
228
- else
229
- styled = white(d);
230
- const paddedLine = padVisible(styled, 58);
231
- console.log(`${darkBorder('│')} ${paddedLine} ${darkBorder('│')}`);
324
+ if (remediation.status === 'UNAVAILABLE') {
325
+ console.log(cyan.bold('▶ AI REMEDIATION · UNAVAILABLE'));
326
+ console.log('');
327
+ console.log(gray(' Reason:'));
328
+ console.log(` ${yellow(remediation.unavailableReason || 'NVIDIA_API_KEY not configured.')}`);
329
+ console.log(` ${dimGray('Scanning, deterministic scoring, and policy enforcement remain 100% operational.')}\n`);
232
330
  }
233
- console.log(darkBorder('└────────────────────────────────────────────────────────────┘'));
234
- console.log(`\n${cyan('Confidence:')} ${green.bold(remediation.confidence + '%')}`);
235
- }
236
- // 5. Summary Bar (bottom capsule)
237
- console.log(`\n${cyan('▶ SUMMARY')}`);
238
- const summaryText = `🛡️ Scan completed in ${duration}${stats.total} findings │ 6 passed`;
239
- const barTop = `┌${'─'.repeat(visibleWidth(summaryText) + 4)}┐`;
240
- const barMid = `│ ${summaryText} │`;
241
- const barBot = `└${''.repeat(visibleWidth(summaryText) + 4)}┘`;
242
- console.log(cyan(barTop));
243
- console.log(cyan(barMid));
244
- console.log(cyan(barBot));
245
- console.log(`\n💡 ${gray('Tip: Run')} ${cyan('`vibeguard watch`')} ${gray('to continuously monitor your codebase.')}\n`);
331
+ else {
332
+ console.log(cyan.bold('▶ AI REMEDIATION · OPTIONAL'));
333
+ console.log('');
334
+ console.log(` ${gray('Finding:')} ${white.bold(remediation.findingId)}`);
335
+ if (remediation.severity) {
336
+ console.log(` ${gray('Severity:')} ${white(remediation.severity)}`);
337
+ }
338
+ console.log('');
339
+ console.log(` ${cyan('ISSUE')}`);
340
+ console.log(` ${white(maskSecrets(remediation.issue))}\n`);
341
+ if (remediation.impact) {
342
+ console.log(` ${cyan('IMPACT')}`);
343
+ console.log(` ${white(maskSecrets(remediation.impact))}\n`);
344
+ }
345
+ if (remediation.recommendation) {
346
+ console.log(` ${cyan('RECOMMENDED FIX')}`);
347
+ console.log(` ${white(maskSecrets(remediation.recommendation))}\n`);
348
+ }
349
+ console.log(` ${cyan('SUGGESTED FIX')}`);
350
+ if (remediation.hasConcretePatch && remediation.suggestedFix) {
351
+ console.log(` ${darkBorder('┌────────────────────────────────────────────────────────────┐')}`);
352
+ const lines = maskSecrets(remediation.suggestedFix).split('\n');
353
+ for (const line of lines) {
354
+ let colored = white(line);
355
+ if (line.trim().startsWith('+'))
356
+ colored = green(line);
357
+ else if (line.trim().startsWith('-'))
358
+ colored = red(line);
359
+ else if (line.trim().startsWith('#') || line.trim().startsWith('//'))
360
+ colored = dimGray(line);
361
+ console.log(` ${darkBorder('│')} ${padVisible(colored, 58)} ${darkBorder('│')}`);
362
+ }
363
+ console.log(` ${darkBorder('└────────────────────────────────────────────────────────────┘')}`);
364
+ if (remediation.confidence) {
365
+ console.log(`\n ${gray('Confidence:')} ${green.bold(remediation.confidence + '%')}`);
366
+ }
367
+ console.log(` ${gray('Status:')} ${white.bold(remediation.status)}`);
368
+ }
369
+ else {
370
+ console.log(` ${yellow('No patch generated — guidance only.')}\n`);
371
+ console.log(` ${gray('Status:')} ${yellow.bold('GUIDANCE ONLY')}`);
372
+ }
373
+ console.log('');
374
+ }
375
+ }
376
+ // 7. Repository Metadata (Directive 13)
377
+ console.log(cyan.bold('▶ REPOSITORY'));
378
+ console.log('');
379
+ console.log(` ${gray('Name'.padEnd(12))} ${white(gitInfo.name)}`);
380
+ console.log(` ${gray('Branch'.padEnd(12))} ${white(gitInfo.branch)}`);
381
+ console.log(` ${gray('Commit'.padEnd(12))} ${white(gitInfo.commit)}`);
382
+ console.log(` ${gray('Duration'.padEnd(12))} ${white(duration)}`);
383
+ if (policyThreshold) {
384
+ console.log(` ${gray('Policy'.padEnd(12))} ${white('fail-on ' + policyThreshold)}`);
385
+ }
386
+ console.log('');
387
+ // 8. Truthful Summary (Directive 14)
388
+ console.log(cyan.bold('▶ SUMMARY'));
389
+ console.log('');
390
+ const evaluatedCount = scanners.length;
391
+ const executedCount = scanners.filter(s => String(s.state).toUpperCase() === 'SUCCESS').length;
392
+ console.log(` ${green('✓')} Scan completed in ${duration}`);
393
+ console.log(` ${green('✓')} ${evaluatedCount} scanners evaluated`);
394
+ console.log(` ${green('✓')} ${executedCount} scanner${executedCount === 1 ? '' : 's'} executed`);
395
+ console.log(` ${green('✓')} ${stats.total} findings detected (${stats.critical} critical, ${stats.high} high, ${stats.medium} medium, ${stats.low} low)`);
396
+ console.log('');
397
+ // 9. Local vs Cloud Synchronization Status (Directive 15)
398
+ console.log(` ${green('✓')} Local scan completed`);
399
+ if (syncStatus === 'SYNCED') {
400
+ console.log(` ${green('✓')} Results synced to VibeGuard Cloud (Tenant Isolated)`);
401
+ }
402
+ else if (syncStatus === 'FAILED') {
403
+ console.log(` ${red('✗')} Cloud sync failed (local result preserved)`);
404
+ }
405
+ else {
406
+ console.log(` ${dimGray('○')} Cloud sync skipped — run 'vibeguard scan --sync' to stream telemetry`);
407
+ }
408
+ // 10. Privacy Guarantee (Directive 16)
409
+ console.log(`\n ${dimGray('Privacy: Source code remains local. Only normalized security metadata is synchronized.')}\n`);
410
+ }
411
+ function renderCIOutput(options) {
412
+ const lines = [];
413
+ const breakdown = options.deterministicScore.breakdown || calculateScore(options.findings);
414
+ lines.push('VibeGuard Security Policy');
415
+ lines.push('');
416
+ lines.push(`Score: ${options.deterministicScore.score}/100 (${options.deterministicScore.grade})`);
417
+ lines.push(`Findings: ${options.findings.length}`);
418
+ lines.push(`Critical: ${breakdown.critical}`);
419
+ lines.push(`High: ${breakdown.high}`);
420
+ lines.push(`Medium: ${breakdown.medium}`);
421
+ lines.push(`Low: ${breakdown.low}`);
422
+ lines.push('');
423
+ lines.push(`Policy: ${options.policyPassed ? 'PASS' : 'FAIL'}`);
424
+ lines.push(`Threshold: ${options.failThreshold.toUpperCase()}`);
425
+ if (options.verbose && options.scanners) {
426
+ lines.push('');
427
+ lines.push('Scanner Telemetry:');
428
+ for (const sr of options.scanners) {
429
+ lines.push(` - ${sr.scanner.padEnd(14)}: ${sr.state.padEnd(14)} (${sr.durationMs || 0}ms) findings: ${sr.findingsCount || 0}`);
430
+ }
431
+ }
432
+ return lines;
433
+ }
434
+ function generateJsonOutput(options) {
435
+ return {
436
+ score: options.deterministicScore.score,
437
+ grade: options.deterministicScore.grade,
438
+ postureStatus: options.postureStatus,
439
+ coverage: {
440
+ assessedDomains: options.activeDomainsCount,
441
+ totalDomains: 7,
442
+ domains: options.coverageData
443
+ },
444
+ deductions: options.deterministicScore.deductions,
445
+ breakdown: options.deterministicScore.breakdown || calculateScore(options.findings),
446
+ explanation: options.deterministicScore.explanation,
447
+ findings: options.findings.map(f => ({
448
+ id: f.id,
449
+ title: maskSecrets(f.title || ''),
450
+ severity: f.severity,
451
+ scanner: f.scanner,
452
+ file: f.file,
453
+ line: f.line,
454
+ ruleId: f.ruleId,
455
+ cwe: f.cwe,
456
+ owasp: f.owasp
457
+ })),
458
+ scanners: options.scanners,
459
+ repository: {
460
+ name: options.gitInfo.name,
461
+ branch: options.gitInfo.branch,
462
+ commit: options.gitInfo.commit
463
+ },
464
+ policyResult: {
465
+ passed: options.policyPassed,
466
+ threshold: options.failThreshold.toUpperCase()
467
+ },
468
+ durationMs: options.durationMs
469
+ };
470
+ }
471
+ function evaluatePolicy(failThreshold, breakdown, totalFindings) {
472
+ const normThreshold = (failThreshold || 'high').toLowerCase();
473
+ let thresholdBreached = false;
474
+ if (normThreshold === 'critical') {
475
+ thresholdBreached = breakdown.critical > 0;
476
+ }
477
+ else if (normThreshold === 'high') {
478
+ thresholdBreached = breakdown.critical > 0 || breakdown.high > 0;
479
+ }
480
+ else if (normThreshold === 'medium') {
481
+ thresholdBreached = breakdown.critical > 0 || breakdown.high > 0 || breakdown.medium > 0;
482
+ }
483
+ else if (normThreshold === 'low') {
484
+ thresholdBreached = totalFindings > 0;
485
+ }
486
+ return {
487
+ passed: !thresholdBreached,
488
+ exitCode: thresholdBreached ? 1 : 0
489
+ };
246
490
  }