@maverick006/vibeguard 1.0.11 → 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 +363 -120
- package/dist/index.js +316 -106
- package/jest.config.js +5 -0
- package/package.json +7 -3
- package/src/credentials.ts +74 -0
- package/src/formatter.ts +444 -142
- 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
|
}
|
|
@@ -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 = '
|
|
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
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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 (
|
|
119
|
+
if (grade === 'F')
|
|
89
120
|
riskLevel = 'CRITICAL RISK';
|
|
90
|
-
else if (
|
|
121
|
+
else if (grade === 'D')
|
|
91
122
|
riskLevel = 'HIGH RISK';
|
|
92
|
-
else if (
|
|
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
|
-
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
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,325 @@ 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
|
-
//
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
console.log(
|
|
142
|
-
console.log(
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
console.log(
|
|
146
|
-
console.log(
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
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
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
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.
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
183
|
-
|
|
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
|
|
194
|
-
const
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
201
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
+
};
|
|
246
489
|
}
|