@maverick006/vibeguard 1.0.10 → 1.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/credentials.js +63 -0
- package/dist/formatter.js +365 -121
- package/dist/index.js +317 -107
- package/jest.config.js +5 -0
- package/package.json +7 -3
- package/src/credentials.ts +74 -0
- package/src/formatter.ts +446 -143
- package/src/index.ts +455 -211
- package/tests/auth.test.ts +96 -0
- package/tests/formatter.test.ts +666 -0
|
@@ -0,0 +1,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;
|
|
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
|
}
|
|
@@ -36,9 +50,10 @@ function padVisible(str, targetWidth) {
|
|
|
36
50
|
return str + ' '.repeat(diff);
|
|
37
51
|
}
|
|
38
52
|
function getGitInfo(cwd = process.cwd()) {
|
|
39
|
-
|
|
53
|
+
const resolvedCwd = path_1.default.resolve(cwd);
|
|
54
|
+
let name = path_1.default.basename(resolvedCwd);
|
|
40
55
|
let branch = 'main';
|
|
41
|
-
let commit = '
|
|
56
|
+
let commit = 'HEAD';
|
|
42
57
|
let remoteUrl = '';
|
|
43
58
|
try {
|
|
44
59
|
const branchOut = (0, child_process_1.execSync)('git rev-parse --abbrev-ref HEAD', { cwd, stdio: ['pipe', 'pipe', 'ignore'] }).toString().trim();
|
|
@@ -62,13 +77,14 @@ function getGitInfo(cwd = process.cwd()) {
|
|
|
62
77
|
}
|
|
63
78
|
}
|
|
64
79
|
catch { }
|
|
65
|
-
return { name, branch, commit, remoteUrl
|
|
80
|
+
return { name, branch, commit, remoteUrl };
|
|
66
81
|
}
|
|
67
82
|
function calculateScore(findings) {
|
|
68
83
|
let critical = 0;
|
|
69
84
|
let high = 0;
|
|
70
85
|
let medium = 0;
|
|
71
86
|
let low = 0;
|
|
87
|
+
let info = 0;
|
|
72
88
|
for (const f of findings) {
|
|
73
89
|
const sev = (f.severity || '').toUpperCase();
|
|
74
90
|
if (sev === 'CRITICAL' || sev === types_1.Severity.CRITICAL)
|
|
@@ -77,31 +93,49 @@ function calculateScore(findings) {
|
|
|
77
93
|
high++;
|
|
78
94
|
else if (sev === 'MEDIUM' || sev === types_1.Severity.MEDIUM)
|
|
79
95
|
medium++;
|
|
80
|
-
else
|
|
96
|
+
else if (sev === 'LOW' || sev === types_1.Severity.LOW)
|
|
81
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);
|
|
82
107
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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';
|
|
86
118
|
let riskLevel = 'LOW RISK';
|
|
87
|
-
if (
|
|
119
|
+
if (grade === 'F')
|
|
88
120
|
riskLevel = 'CRITICAL RISK';
|
|
89
|
-
else if (
|
|
121
|
+
else if (grade === 'D')
|
|
90
122
|
riskLevel = 'HIGH RISK';
|
|
91
|
-
else if (
|
|
123
|
+
else if (grade === 'C' || grade === 'B')
|
|
92
124
|
riskLevel = 'MEDIUM RISK';
|
|
93
125
|
return {
|
|
94
126
|
critical,
|
|
95
127
|
high,
|
|
96
128
|
medium,
|
|
97
129
|
low,
|
|
130
|
+
info,
|
|
98
131
|
total: findings.length,
|
|
99
132
|
score,
|
|
133
|
+
grade,
|
|
100
134
|
riskLevel
|
|
101
135
|
};
|
|
102
136
|
}
|
|
103
137
|
function renderDashboard(options) {
|
|
104
|
-
const { findings, stats, gitInfo, duration, remediation } = options;
|
|
138
|
+
const { findings, stats, deterministicScore, coverage, gitInfo, duration, scanners, remediation, syncStatus, policyThreshold, verbose } = options;
|
|
105
139
|
const cyan = chalk_1.default.hex('#00E5FF');
|
|
106
140
|
const gray = chalk_1.default.hex('#94A3B8');
|
|
107
141
|
const dimGray = chalk_1.default.hex('#475569');
|
|
@@ -111,18 +145,19 @@ function renderDashboard(options) {
|
|
|
111
145
|
const orange = chalk_1.default.hex('#F97316');
|
|
112
146
|
const yellow = chalk_1.default.hex('#F59E0B');
|
|
113
147
|
const white = chalk_1.default.white;
|
|
114
|
-
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
'
|
|
119
|
-
'
|
|
120
|
-
'
|
|
121
|
-
'
|
|
122
|
-
'
|
|
123
|
-
' `-------\' '
|
|
148
|
+
// Active domains count (out of 7)
|
|
149
|
+
const domainKeys = [
|
|
150
|
+
'code',
|
|
151
|
+
'dependencies',
|
|
152
|
+
'secrets',
|
|
153
|
+
'containers',
|
|
154
|
+
'iac',
|
|
155
|
+
'web',
|
|
156
|
+
'cloud'
|
|
124
157
|
];
|
|
125
|
-
|
|
158
|
+
const activeDomainsCount = domainKeys.filter(k => coverage[k]).length;
|
|
159
|
+
const isPartial = activeDomainsCount < 7;
|
|
160
|
+
// Title ASCII Art
|
|
126
161
|
const title = [
|
|
127
162
|
' __ __ ___ ____ _____ ____ _ _ _ ____ ____ ',
|
|
128
163
|
'\\ \\ / /|_ _| __ )| ____/ ___| | | | / \\ | _ \\| _ \\ ',
|
|
@@ -130,116 +165,325 @@ function renderDashboard(options) {
|
|
|
130
165
|
' \\ V / | || |_) | |___| |_| |_| / ___ \\ _ <| |_| |',
|
|
131
166
|
' \\_/ |___|____/|_____|\\____|\\___/_/ \\_\\_| \\_\\____/'
|
|
132
167
|
];
|
|
133
|
-
const scoreColor = stats.score >= 85 ? green : stats.score >= 65 ? yellow : red;
|
|
134
|
-
const riskColor = stats.riskLevel === 'LOW RISK' ? green : stats.riskLevel === 'MEDIUM RISK' ? yellow : red;
|
|
135
168
|
console.log('\n');
|
|
136
|
-
//
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
console.log(
|
|
141
|
-
console.log(
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
console.log(
|
|
145
|
-
console.log(
|
|
146
|
-
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
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('│')}`);
|
|
150
189
|
console.log(`${darkBorder('│')}${' '.repeat(boxWidth)}${darkBorder('│')}`);
|
|
151
|
-
const
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
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('│')}`);
|
|
155
198
|
console.log(darkBorder(`└${'─'.repeat(boxWidth)}┘`));
|
|
156
199
|
console.log('');
|
|
157
|
-
// 3.
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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' }
|
|
172
246
|
];
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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.')}`);
|
|
179
296
|
}
|
|
180
297
|
else {
|
|
181
|
-
|
|
182
|
-
|
|
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')}`;
|
|
183
303
|
const sev = (f.severity || 'LOW').toUpperCase();
|
|
184
|
-
let sevFormatted = cyan('LOW
|
|
304
|
+
let sevFormatted = cyan('LOW ');
|
|
185
305
|
if (sev === 'CRITICAL')
|
|
186
|
-
sevFormatted = red.bold('CRITICAL
|
|
306
|
+
sevFormatted = red.bold('CRITICAL');
|
|
187
307
|
else if (sev === 'HIGH')
|
|
188
|
-
sevFormatted = orange.bold('HIGH
|
|
308
|
+
sevFormatted = orange.bold('HIGH ');
|
|
189
309
|
else if (sev === 'MEDIUM')
|
|
190
|
-
sevFormatted = yellow('MEDIUM
|
|
191
|
-
const rawTitle = f.title || 'Security Finding';
|
|
192
|
-
const
|
|
193
|
-
const
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
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`)}`);
|
|
197
319
|
}
|
|
198
320
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const headerCol2 = cyan('▶ TOP FINDINGS');
|
|
202
|
-
console.log(`${headerCol1} ${headerCol2}`);
|
|
203
|
-
const maxRows = Math.max(pipelineRows.length, findingRows.length);
|
|
204
|
-
for (let i = 0; i < maxRows; i++) {
|
|
205
|
-
const left = padVisible(pipelineRows[i] || '', 36);
|
|
206
|
-
const right = findingRows[i] || '';
|
|
207
|
-
console.log(`${left} ${right}`);
|
|
208
|
-
}
|
|
209
|
-
// 4. AI Remediation Section
|
|
321
|
+
console.log('');
|
|
322
|
+
// 6. Structured AI Remediation Section (Directive 10 & 11)
|
|
210
323
|
if (remediation) {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
console.log(darkBorder('┌────────────────────────────────────────────────────────────┐'));
|
|
218
|
-
const diffLines = remediation.diffSnippet.split('\n');
|
|
219
|
-
for (const d of diffLines) {
|
|
220
|
-
let styled = d;
|
|
221
|
-
if (d.trim().startsWith('-'))
|
|
222
|
-
styled = red(d);
|
|
223
|
-
else if (d.trim().startsWith('+'))
|
|
224
|
-
styled = green(d);
|
|
225
|
-
else if (d.trim().startsWith('#'))
|
|
226
|
-
styled = dimGray(d);
|
|
227
|
-
else
|
|
228
|
-
styled = white(d);
|
|
229
|
-
const paddedLine = padVisible(styled, 58);
|
|
230
|
-
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`);
|
|
231
330
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
+
lines.push('VibeGuard Security Policy');
|
|
414
|
+
lines.push('');
|
|
415
|
+
lines.push(`Score: ${options.deterministicScore.score}/100 (${options.deterministicScore.grade})`);
|
|
416
|
+
lines.push(`Findings: ${options.findings.length}`);
|
|
417
|
+
lines.push(`Critical: ${options.deterministicScore.breakdown.critical}`);
|
|
418
|
+
lines.push(`High: ${options.deterministicScore.breakdown.high}`);
|
|
419
|
+
lines.push(`Medium: ${options.deterministicScore.breakdown.medium}`);
|
|
420
|
+
lines.push(`Low: ${options.deterministicScore.breakdown.low}`);
|
|
421
|
+
lines.push('');
|
|
422
|
+
lines.push(`Policy: ${options.policyPassed ? 'PASS' : 'FAIL'}`);
|
|
423
|
+
lines.push(`Threshold: ${options.failThreshold.toUpperCase()}`);
|
|
424
|
+
if (options.verbose && options.scanners) {
|
|
425
|
+
lines.push('');
|
|
426
|
+
lines.push('Scanner Telemetry:');
|
|
427
|
+
for (const sr of options.scanners) {
|
|
428
|
+
lines.push(` - ${sr.scanner.padEnd(14)}: ${sr.state.padEnd(14)} (${sr.durationMs || 0}ms) findings: ${sr.findingsCount || 0}`);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return lines;
|
|
432
|
+
}
|
|
433
|
+
function generateJsonOutput(options) {
|
|
434
|
+
return {
|
|
435
|
+
score: options.deterministicScore.score,
|
|
436
|
+
grade: options.deterministicScore.grade,
|
|
437
|
+
postureStatus: options.postureStatus,
|
|
438
|
+
coverage: {
|
|
439
|
+
assessedDomains: options.activeDomainsCount,
|
|
440
|
+
totalDomains: 7,
|
|
441
|
+
domains: options.coverageData
|
|
442
|
+
},
|
|
443
|
+
deductions: options.deterministicScore.deductions,
|
|
444
|
+
breakdown: options.deterministicScore.breakdown,
|
|
445
|
+
explanation: options.deterministicScore.explanation,
|
|
446
|
+
findings: options.findings.map(f => ({
|
|
447
|
+
id: f.id,
|
|
448
|
+
title: maskSecrets(f.title || ''),
|
|
449
|
+
severity: f.severity,
|
|
450
|
+
scanner: f.scanner,
|
|
451
|
+
file: f.file,
|
|
452
|
+
line: f.line,
|
|
453
|
+
ruleId: f.ruleId,
|
|
454
|
+
cwe: f.cwe,
|
|
455
|
+
owasp: f.owasp
|
|
456
|
+
})),
|
|
457
|
+
scanners: options.scanners,
|
|
458
|
+
repository: {
|
|
459
|
+
name: options.gitInfo.name,
|
|
460
|
+
branch: options.gitInfo.branch,
|
|
461
|
+
commit: options.gitInfo.commit
|
|
462
|
+
},
|
|
463
|
+
policyResult: {
|
|
464
|
+
passed: options.policyPassed,
|
|
465
|
+
threshold: options.failThreshold.toUpperCase()
|
|
466
|
+
},
|
|
467
|
+
durationMs: options.durationMs
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
function evaluatePolicy(failThreshold, breakdown, totalFindings) {
|
|
471
|
+
const normThreshold = (failThreshold || 'high').toLowerCase();
|
|
472
|
+
let thresholdBreached = false;
|
|
473
|
+
if (normThreshold === 'critical') {
|
|
474
|
+
thresholdBreached = breakdown.critical > 0;
|
|
475
|
+
}
|
|
476
|
+
else if (normThreshold === 'high') {
|
|
477
|
+
thresholdBreached = breakdown.critical > 0 || breakdown.high > 0;
|
|
478
|
+
}
|
|
479
|
+
else if (normThreshold === 'medium') {
|
|
480
|
+
thresholdBreached = breakdown.critical > 0 || breakdown.high > 0 || breakdown.medium > 0;
|
|
481
|
+
}
|
|
482
|
+
else if (normThreshold === 'low') {
|
|
483
|
+
thresholdBreached = totalFindings > 0;
|
|
484
|
+
}
|
|
485
|
+
return {
|
|
486
|
+
passed: !thresholdBreached,
|
|
487
|
+
exitCode: thresholdBreached ? 1 : 0
|
|
488
|
+
};
|
|
245
489
|
}
|