@maverick006/vibeguard 1.0.6 → 1.0.8
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/formatter.js +30 -35
- package/dist/index.js +103 -142
- package/package.json +6 -1
- package/src/formatter.ts +32 -37
- package/src/index.ts +106 -145
package/dist/formatter.js
CHANGED
|
@@ -18,6 +18,8 @@ function visibleWidth(str) {
|
|
|
18
18
|
let width = 0;
|
|
19
19
|
for (const char of plain) {
|
|
20
20
|
const code = char.codePointAt(0) || 0;
|
|
21
|
+
if (code === 0xFE0F || code === 0xFE0E)
|
|
22
|
+
continue; // ignore variation selectors
|
|
21
23
|
// Emojis and certain symbols take 2 terminal columns
|
|
22
24
|
if (code > 0x1F000 || (code >= 0x2600 && code <= 0x27BF)) {
|
|
23
25
|
width += 2;
|
|
@@ -130,39 +132,32 @@ function renderDashboard(options) {
|
|
|
130
132
|
const riskColor = stats.riskLevel === 'LOW RISK' ? green : stats.riskLevel === 'MEDIUM RISK' ? yellow : red;
|
|
131
133
|
console.log('\n');
|
|
132
134
|
// Print Top Row: Clock aligned right
|
|
133
|
-
console.log(' '.repeat(
|
|
134
|
-
//
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
`${darkBorder('└───────────────────────────────────────┘')}`
|
|
153
|
-
];
|
|
154
|
-
for (let i = 0; i < leftHeader.length; i++) {
|
|
155
|
-
const l = padVisible(leftHeader[i], 66);
|
|
156
|
-
const r = rightBox[i] || '';
|
|
157
|
-
console.log(`${l}${r}`);
|
|
158
|
-
}
|
|
135
|
+
console.log(' '.repeat(65) + cyan.bold(timeStr));
|
|
136
|
+
// 1. Header (Shield + VIBEGUARD)
|
|
137
|
+
console.log(`${cyan(shield[0])} ${cyan.bold(title[0])}`);
|
|
138
|
+
console.log(`${cyan(shield[1])} ${cyan.bold(title[1])}`);
|
|
139
|
+
console.log(`${cyan(shield[2])} ${cyan.bold(title[2])}`);
|
|
140
|
+
console.log(`${cyan(shield[3])} ${cyan.bold(title[3])}`);
|
|
141
|
+
console.log(`${cyan(shield[4])} ${cyan.bold(title[4])}`);
|
|
142
|
+
console.log(`${cyan(shield[5])} ${gray('AI-Powered DevSecOps Orchestrator')}`);
|
|
143
|
+
console.log(` ${cyan('Scanning. Analyzing. Protecting.')}\n`);
|
|
144
|
+
// 2. Risk Score Box (Placed cleanly BELOW VIBEGUARD Header)
|
|
145
|
+
const boxWidth = 74;
|
|
146
|
+
console.log(darkBorder(`┌${'─'.repeat(boxWidth)}┐`));
|
|
147
|
+
console.log(`${darkBorder('│')} ${cyan.bold('OVERALL RISK SCORE')}${' '.repeat(boxWidth - 20)}${darkBorder('│')}`);
|
|
148
|
+
console.log(`${darkBorder('│')}${' '.repeat(boxWidth)}${darkBorder('│')}`);
|
|
149
|
+
const scoreLine1 = ` ${scoreColor.bold(String(stats.score))} ${gray('/100')} ${red('CRITICAL')} ${String(stats.critical).padEnd(2, ' ')} ${orange('HIGH')} ${String(stats.high).padEnd(2, ' ')}`;
|
|
150
|
+
const scoreLine2 = ` ${riskColor.bold(stats.riskLevel.padEnd(13, ' '))} ${yellow('MEDIUM')} ${String(stats.medium).padEnd(2, ' ')} ${cyan('LOW')} ${String(stats.low).padEnd(2, ' ')}`;
|
|
151
|
+
console.log(`${darkBorder('│')}${padVisible(scoreLine1, boxWidth)}${darkBorder('│')}`);
|
|
152
|
+
console.log(`${darkBorder('│')}${padVisible(scoreLine2, boxWidth)}${darkBorder('│')}`);
|
|
153
|
+
console.log(darkBorder(`└${'─'.repeat(boxWidth)}┘`));
|
|
159
154
|
console.log('');
|
|
160
|
-
// 3
|
|
155
|
+
// 3. Multi-panel Grid (Pipeline & Findings)
|
|
161
156
|
const pipelineRows = [
|
|
162
|
-
|
|
157
|
+
`</> SAST (Semgrep) ${green('✓ OK')}`,
|
|
163
158
|
`📦 Dependency Check (Trivy) ${green('✓ OK')}`,
|
|
164
159
|
`🔑 Secrets Scan (Gitleaks) ${green('✓ OK')}`,
|
|
165
|
-
|
|
160
|
+
`🔒 IaC Scan (Checkov) ${green('✓ OK')}`,
|
|
166
161
|
`🐳 Container Scan (Trivy) ${green('✓ OK')}`,
|
|
167
162
|
`📑 Code Quality (ESLint) ${green('✓ OK')}`,
|
|
168
163
|
``,
|
|
@@ -175,7 +170,7 @@ function renderDashboard(options) {
|
|
|
175
170
|
];
|
|
176
171
|
// Table of findings (top 10)
|
|
177
172
|
const topFindings = findings.slice(0, 10);
|
|
178
|
-
const tableHeader = `${dimGray(padVisible('ID',
|
|
173
|
+
const tableHeader = `${dimGray(padVisible('ID', 13))} ${dimGray(padVisible('SEVERITY', 10))} ${dimGray(padVisible('TITLE', 28))} ${dimGray('FILE:LINE')}`;
|
|
179
174
|
const findingRows = [tableHeader];
|
|
180
175
|
if (topFindings.length === 0) {
|
|
181
176
|
findingRows.push(green(' ✓ No security vulnerabilities detected. Codebase is clean!'));
|
|
@@ -192,24 +187,24 @@ function renderDashboard(options) {
|
|
|
192
187
|
else if (sev === 'MEDIUM')
|
|
193
188
|
sevFormatted = yellow('MEDIUM ');
|
|
194
189
|
const rawTitle = f.title || 'Security Finding';
|
|
195
|
-
const titleTruncated = rawTitle.length >
|
|
190
|
+
const titleTruncated = rawTitle.length > 26 ? rawTitle.slice(0, 24) + '..' : rawTitle;
|
|
196
191
|
const titleFormatted = padVisible(white(titleTruncated), 28);
|
|
197
192
|
const fileLine = `${f.file || 'unknown'}:${f.line || 1}`;
|
|
198
|
-
const idFormatted = padVisible(cyan(id),
|
|
193
|
+
const idFormatted = padVisible(cyan(id), 13);
|
|
199
194
|
findingRows.push(`${idFormatted} ${sevFormatted} ${titleFormatted} ${dimGray(fileLine)}`);
|
|
200
195
|
}
|
|
201
196
|
}
|
|
202
197
|
// Print Section Headers with ANSI-safe padding
|
|
203
198
|
const headerCol1 = padVisible(cyan('▶ SCANNER PIPELINE'), 36);
|
|
204
199
|
const headerCol2 = cyan('▶ TOP FINDINGS');
|
|
205
|
-
console.log(
|
|
200
|
+
console.log(`${headerCol1} ${headerCol2}`);
|
|
206
201
|
const maxRows = Math.max(pipelineRows.length, findingRows.length);
|
|
207
202
|
for (let i = 0; i < maxRows; i++) {
|
|
208
203
|
const left = padVisible(pipelineRows[i] || '', 36);
|
|
209
204
|
const right = findingRows[i] || '';
|
|
210
205
|
console.log(`${left} ${right}`);
|
|
211
206
|
}
|
|
212
|
-
// AI Remediation Section
|
|
207
|
+
// 4. AI Remediation Section
|
|
213
208
|
if (remediation) {
|
|
214
209
|
console.log(`\n${cyan('▶ AI REMEDIATION (VG-AI)')}\n`);
|
|
215
210
|
console.log(`${cyan('Finding:')} ${red.bold(remediation.findingId)}\n`);
|
|
@@ -235,7 +230,7 @@ function renderDashboard(options) {
|
|
|
235
230
|
console.log(darkBorder('└────────────────────────────────────────────────────────────┘'));
|
|
236
231
|
console.log(`\n${cyan('Confidence:')} ${green.bold(remediation.confidence + '%')}`);
|
|
237
232
|
}
|
|
238
|
-
// Summary Bar (bottom capsule)
|
|
233
|
+
// 5. Summary Bar (bottom capsule)
|
|
239
234
|
console.log(`\n${cyan('▶ SUMMARY')}`);
|
|
240
235
|
const summaryText = `🛡️ Scan completed in ${duration} │ ${stats.total} findings │ 6 passed`;
|
|
241
236
|
const barTop = `┌${'─'.repeat(visibleWidth(summaryText) + 4)}┐`;
|
package/dist/index.js
CHANGED
|
@@ -23,20 +23,36 @@ program
|
|
|
23
23
|
.description('Run a security scan on the current directory or target path')
|
|
24
24
|
.option('-d, --dir <path>', 'Directory to scan', process.cwd())
|
|
25
25
|
.option('--fix', 'Automatically generate AI remediation fixes and interactive diff')
|
|
26
|
-
.option('--
|
|
26
|
+
.option('--ci', 'Run in non-interactive CI mode and exit with policy status code')
|
|
27
27
|
.action(async (targetPath, options) => {
|
|
28
28
|
const scanDir = targetPath || options.dir || process.cwd();
|
|
29
29
|
const startTime = Date.now();
|
|
30
|
+
let hasSystemError = false;
|
|
31
|
+
// Use a minimal spinner if in CI mode, or bypass ora entirely if preferred.
|
|
32
|
+
// For simplicity, we just won't render the interactive dashboard if --ci is set.
|
|
30
33
|
const spinner = (0, ora_1.default)({
|
|
31
34
|
text: chalk_1.default.hex('#00E5FF')('Scanning repository for vulnerabilities (SAST, SCA, Secrets, IaC)...'),
|
|
32
|
-
spinner: 'dots'
|
|
35
|
+
spinner: 'dots',
|
|
36
|
+
isSilent: options.ci
|
|
33
37
|
}).start();
|
|
34
38
|
let findings = [];
|
|
35
39
|
try {
|
|
36
|
-
// Initialize the security orchestrator and
|
|
40
|
+
// Initialize the security orchestrator and scanners
|
|
37
41
|
const { Orchestrator } = require('@maverick006/security-engine');
|
|
38
42
|
const { NpmAuditScanner } = require('@maverick006/scanner-npm-audit');
|
|
39
|
-
const
|
|
43
|
+
const { TrivyScanner } = require('@maverick006/scanner-trivy');
|
|
44
|
+
const { SemgrepScanner } = require('@maverick006/scanner-semgrep');
|
|
45
|
+
const { GitleaksScanner } = require('@maverick006/scanner-gitleaks');
|
|
46
|
+
const { CheckovScanner } = require('@maverick006/scanner-checkov');
|
|
47
|
+
const { ZapScanner } = require('@maverick006/scanner-zap');
|
|
48
|
+
const orchestrator = new Orchestrator([
|
|
49
|
+
new NpmAuditScanner(),
|
|
50
|
+
new TrivyScanner(),
|
|
51
|
+
new SemgrepScanner(),
|
|
52
|
+
new GitleaksScanner(),
|
|
53
|
+
new CheckovScanner(),
|
|
54
|
+
new ZapScanner()
|
|
55
|
+
]);
|
|
40
56
|
const scanResult = await orchestrator.runScan({
|
|
41
57
|
scanId: `scan-${Date.now()}`,
|
|
42
58
|
repositoryUrl: 'local',
|
|
@@ -46,108 +62,8 @@ program
|
|
|
46
62
|
findings = scanResult.findings || [];
|
|
47
63
|
}
|
|
48
64
|
catch (err) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
// If demo mode or no findings found in empty directory, provide rich showcase findings matching design
|
|
52
|
-
if (options.demo || findings.length === 0) {
|
|
53
|
-
findings = [
|
|
54
|
-
{
|
|
55
|
-
id: 'VG-CRIT-001',
|
|
56
|
-
title: 'Hardcoded AWS Secret Key',
|
|
57
|
-
severity: types_1.Severity.CRITICAL,
|
|
58
|
-
scanner: 'Gitleaks',
|
|
59
|
-
file: 'config/aws.py',
|
|
60
|
-
line: 12,
|
|
61
|
-
description: 'AWS Secret Key is hardcoded in the source code.',
|
|
62
|
-
codeSnippet: 'AWS_SECRET_KEY = "AKIAIOSFODNN7EXAMPLE"'
|
|
63
|
-
},
|
|
64
|
-
{
|
|
65
|
-
id: 'VG-CRIT-002',
|
|
66
|
-
title: 'Exposed JWT Secret',
|
|
67
|
-
severity: types_1.Severity.CRITICAL,
|
|
68
|
-
scanner: 'Gitleaks',
|
|
69
|
-
file: 'config/auth.py',
|
|
70
|
-
line: 8,
|
|
71
|
-
description: 'Hardcoded JWT secret token detected.',
|
|
72
|
-
codeSnippet: 'const JWT_SECRET = "super_secret_123"'
|
|
73
|
-
},
|
|
74
|
-
{
|
|
75
|
-
id: 'VG-HIGH-003',
|
|
76
|
-
title: 'SQL Injection Risk',
|
|
77
|
-
severity: types_1.Severity.HIGH,
|
|
78
|
-
scanner: 'Semgrep',
|
|
79
|
-
file: 'api/users.py',
|
|
80
|
-
line: 45,
|
|
81
|
-
description: 'Direct concatenation of user input into SQL query.',
|
|
82
|
-
codeSnippet: 'query = "SELECT * FROM users WHERE name = " + req.query.name'
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
id: 'VG-HIGH-004',
|
|
86
|
-
title: 'Outdated Dependency (lodash)',
|
|
87
|
-
severity: types_1.Severity.HIGH,
|
|
88
|
-
scanner: 'npm-audit',
|
|
89
|
-
file: 'package.json',
|
|
90
|
-
line: 23,
|
|
91
|
-
description: 'Vulnerable prototype pollution in lodash version.',
|
|
92
|
-
codeSnippet: '"lodash": "4.17.15"'
|
|
93
|
-
},
|
|
94
|
-
{
|
|
95
|
-
id: 'VG-HIGH-005',
|
|
96
|
-
title: 'Unsafe Deserialization',
|
|
97
|
-
severity: types_1.Severity.HIGH,
|
|
98
|
-
scanner: 'Semgrep',
|
|
99
|
-
file: 'utils/parser.py',
|
|
100
|
-
line: 78,
|
|
101
|
-
description: 'Unsafe pickle.loads execution.',
|
|
102
|
-
codeSnippet: 'data = pickle.loads(user_input)'
|
|
103
|
-
},
|
|
104
|
-
{
|
|
105
|
-
id: 'VG-MED-006',
|
|
106
|
-
title: 'S3 Bucket Public Read',
|
|
107
|
-
severity: types_1.Severity.MEDIUM,
|
|
108
|
-
scanner: 'Checkov',
|
|
109
|
-
file: 'iac/s3.tf',
|
|
110
|
-
line: 14,
|
|
111
|
-
description: 'Public read access enabled on production storage bucket.',
|
|
112
|
-
codeSnippet: 'acl = "public-read"'
|
|
113
|
-
},
|
|
114
|
-
{
|
|
115
|
-
id: 'VG-MED-007',
|
|
116
|
-
title: 'Missing Rate Limiting',
|
|
117
|
-
severity: types_1.Severity.MEDIUM,
|
|
118
|
-
scanner: 'Semgrep',
|
|
119
|
-
file: 'api/auth.py',
|
|
120
|
-
line: 32,
|
|
121
|
-
description: 'Authentication endpoint lacks rate limiting protection.'
|
|
122
|
-
},
|
|
123
|
-
{
|
|
124
|
-
id: 'VG-MED-008',
|
|
125
|
-
title: 'CORS Misconfiguration',
|
|
126
|
-
severity: types_1.Severity.MEDIUM,
|
|
127
|
-
scanner: 'Semgrep',
|
|
128
|
-
file: 'web/middleware.py',
|
|
129
|
-
line: 19,
|
|
130
|
-
description: 'Wildcard CORS origin enabled in production middleware.'
|
|
131
|
-
},
|
|
132
|
-
{
|
|
133
|
-
id: 'VG-LOW-009',
|
|
134
|
-
title: 'Unused Dependency',
|
|
135
|
-
severity: types_1.Severity.LOW,
|
|
136
|
-
scanner: 'npm-audit',
|
|
137
|
-
file: 'package.json',
|
|
138
|
-
line: 102,
|
|
139
|
-
description: 'Unused package detected.'
|
|
140
|
-
},
|
|
141
|
-
{
|
|
142
|
-
id: 'VG-LOW-010',
|
|
143
|
-
title: 'Missing Security Headers',
|
|
144
|
-
severity: types_1.Severity.LOW,
|
|
145
|
-
scanner: 'Zap',
|
|
146
|
-
file: 'web/server.py',
|
|
147
|
-
line: 56,
|
|
148
|
-
description: 'Strict-Transport-Security header is not set.'
|
|
149
|
-
}
|
|
150
|
-
];
|
|
65
|
+
console.error(chalk_1.default.red('Orchestrator failed to run scans.'), err);
|
|
66
|
+
hasSystemError = true;
|
|
151
67
|
}
|
|
152
68
|
spinner.stop();
|
|
153
69
|
const elapsedMs = Date.now() - startTime;
|
|
@@ -157,12 +73,12 @@ program
|
|
|
157
73
|
const stats = (0, formatter_1.calculateScore)(findings);
|
|
158
74
|
const gitInfo = (0, formatter_1.getGitInfo)(scanDir);
|
|
159
75
|
let remediationData = undefined;
|
|
160
|
-
// Generate AI Remediation
|
|
161
|
-
if (options.fix || true) {
|
|
76
|
+
// Generate AI Remediation only if not in CI mode to save time, or if explicitly asked
|
|
77
|
+
if (findings.length > 0 && !options.ci && (options.fix || true)) {
|
|
162
78
|
try {
|
|
163
79
|
const explainer = new ai_engine_1.ContextualExplainer();
|
|
164
80
|
const primaryFinding = findings[0];
|
|
165
|
-
let snippet = primaryFinding.codeSnippet || '
|
|
81
|
+
let snippet = primaryFinding.codeSnippet || '';
|
|
166
82
|
if (primaryFinding.file && (0, fs_1.existsSync)((0, path_1.join)(scanDir, primaryFinding.file))) {
|
|
167
83
|
const content = (0, fs_1.readFileSync)((0, path_1.join)(scanDir, primaryFinding.file), 'utf-8');
|
|
168
84
|
const lines = content.split('\n');
|
|
@@ -170,44 +86,89 @@ program
|
|
|
170
86
|
snippet = lines.slice(Math.max(0, targetLine - 3), targetLine + 3).join('\n');
|
|
171
87
|
}
|
|
172
88
|
const explanation = await explainer.explainFinding(primaryFinding, { codeContext: snippet });
|
|
173
|
-
const diffSnippet = [
|
|
174
|
-
'10 # config/aws.py',
|
|
175
|
-
'- 11 AWS_SECRET_KEY = "AKIAIOSFODNN7EXAMPLE"',
|
|
176
|
-
'+ 12 AWS_SECRET_KEY = os.getenv("AWS_SECRET_KEY")',
|
|
177
|
-
'13'
|
|
178
|
-
].join('\n');
|
|
179
89
|
remediationData = {
|
|
180
|
-
findingId: primaryFinding.id
|
|
181
|
-
issue: explanation.summary
|
|
182
|
-
impact:
|
|
183
|
-
recommendation: explanation.remediation
|
|
184
|
-
diffSnippet: explanation.codeFix
|
|
185
|
-
confidence:
|
|
90
|
+
findingId: primaryFinding.id,
|
|
91
|
+
issue: explanation.summary,
|
|
92
|
+
impact: explanation.details || 'Potential security impact based on context.',
|
|
93
|
+
recommendation: explanation.remediation,
|
|
94
|
+
diffSnippet: explanation.codeFix || '',
|
|
95
|
+
confidence: 90
|
|
186
96
|
};
|
|
187
97
|
}
|
|
188
98
|
catch (e) {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
impact: 'This can lead to unauthorized access to your AWS resources.',
|
|
193
|
-
recommendation: 'Use environment variables or AWS Secrets Manager to store secrets.',
|
|
194
|
-
diffSnippet: [
|
|
195
|
-
'10 # config/aws.py',
|
|
196
|
-
'- 11 AWS_SECRET_KEY = "AKIAIOSFODNN7EXAMPLE"',
|
|
197
|
-
'+ 12 AWS_SECRET_KEY = os.getenv("AWS_SECRET_KEY")',
|
|
198
|
-
'13'
|
|
199
|
-
].join('\n'),
|
|
200
|
-
confidence: 98
|
|
201
|
-
};
|
|
99
|
+
if (!options.ci) {
|
|
100
|
+
console.error(chalk_1.default.yellow('AI Remediation generation failed.'), e);
|
|
101
|
+
}
|
|
202
102
|
}
|
|
203
103
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
104
|
+
if (!options.ci) {
|
|
105
|
+
// Render the beautiful cyber dashboard interactively
|
|
106
|
+
(0, formatter_1.renderDashboard)({
|
|
107
|
+
findings,
|
|
108
|
+
stats,
|
|
109
|
+
gitInfo,
|
|
110
|
+
duration,
|
|
111
|
+
remediation: remediationData
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
// CI Output
|
|
116
|
+
console.log(`VibeGuard CI Scan Complete. Duration: ${duration}`);
|
|
117
|
+
console.log(`Findings: ${findings.length}`);
|
|
118
|
+
console.log(`Risk Score: ${stats.score}/100 (${stats.riskLevel})`);
|
|
119
|
+
const critical = findings.filter(f => (f.severity || '').toUpperCase() === types_1.Severity.CRITICAL).length;
|
|
120
|
+
const high = findings.filter(f => (f.severity || '').toUpperCase() === types_1.Severity.HIGH).length;
|
|
121
|
+
console.log(`Critical: ${critical}, High: ${high}`);
|
|
122
|
+
}
|
|
123
|
+
// Sync with Web Dashboard
|
|
124
|
+
const syncSpinner = (0, ora_1.default)({
|
|
125
|
+
text: chalk_1.default.dim('Syncing results to VibeGuard Dashboard...'),
|
|
126
|
+
spinner: 'dots',
|
|
127
|
+
isSilent: options.ci
|
|
128
|
+
}).start();
|
|
129
|
+
try {
|
|
130
|
+
const API_URL = process.env.VIBEGUARD_API_URL || 'http://localhost:3001';
|
|
131
|
+
const API_KEY = process.env.VIBEGUARD_API_KEY;
|
|
132
|
+
const response = await fetch(`${API_URL}/api/scans/upload`, {
|
|
133
|
+
method: 'POST',
|
|
134
|
+
headers: {
|
|
135
|
+
'Content-Type': 'application/json',
|
|
136
|
+
'Authorization': `Bearer ${API_KEY}`
|
|
137
|
+
},
|
|
138
|
+
body: JSON.stringify({
|
|
139
|
+
repositoryName: gitInfo.name || 'Local Project',
|
|
140
|
+
repositoryUrl: gitInfo.name || 'local',
|
|
141
|
+
numericScore: stats.score,
|
|
142
|
+
score: stats.riskLevel,
|
|
143
|
+
findings: findings
|
|
144
|
+
})
|
|
145
|
+
});
|
|
146
|
+
if (response.ok) {
|
|
147
|
+
syncSpinner.succeed(chalk_1.default.dim('Results synced to dashboard.'));
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
syncSpinner.fail(chalk_1.default.dim('Failed to sync results to dashboard.'));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
catch (e) {
|
|
154
|
+
syncSpinner.warn(chalk_1.default.dim('Dashboard API unreachable. Skipping sync.'));
|
|
155
|
+
}
|
|
156
|
+
// Exit codes
|
|
157
|
+
if (hasSystemError) {
|
|
158
|
+
process.exit(2);
|
|
159
|
+
}
|
|
160
|
+
// Policy fail if we have CRITICAL or HIGH findings
|
|
161
|
+
const criticalOrHighCount = findings.filter(f => {
|
|
162
|
+
const s = (f.severity || '').toUpperCase();
|
|
163
|
+
return s === types_1.Severity.CRITICAL || s === types_1.Severity.HIGH;
|
|
164
|
+
}).length;
|
|
165
|
+
if (criticalOrHighCount > 0) {
|
|
166
|
+
if (options.ci)
|
|
167
|
+
console.error(chalk_1.default.red('Security Policy FAILED.'));
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
if (options.ci)
|
|
171
|
+
console.log(chalk_1.default.green('Security Policy PASSED.'));
|
|
172
|
+
process.exit(0);
|
|
212
173
|
});
|
|
213
174
|
program.parse(process.argv);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maverick006/vibeguard",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
4
4
|
"description": "VibeGuard CLI",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -15,6 +15,11 @@
|
|
|
15
15
|
"@maverick006/ai-engine": "*",
|
|
16
16
|
"@maverick006/security-engine": "*",
|
|
17
17
|
"@maverick006/scanner-npm-audit": "*",
|
|
18
|
+
"@maverick006/scanner-trivy": "*",
|
|
19
|
+
"@maverick006/scanner-semgrep": "*",
|
|
20
|
+
"@maverick006/scanner-gitleaks": "*",
|
|
21
|
+
"@maverick006/scanner-checkov": "*",
|
|
22
|
+
"@maverick006/scanner-zap": "*",
|
|
18
23
|
"@maverick006/types": "*",
|
|
19
24
|
"chalk": "^4.1.2",
|
|
20
25
|
"commander": "^11.1.0",
|
package/src/formatter.ts
CHANGED
|
@@ -22,6 +22,7 @@ function visibleWidth(str: string): number {
|
|
|
22
22
|
let width = 0;
|
|
23
23
|
for (const char of plain) {
|
|
24
24
|
const code = char.codePointAt(0) || 0;
|
|
25
|
+
if (code === 0xFE0F || code === 0xFE0E) continue; // ignore variation selectors
|
|
25
26
|
// Emojis and certain symbols take 2 terminal columns
|
|
26
27
|
if (code > 0x1F000 || (code >= 0x2600 && code <= 0x27BF)) {
|
|
27
28
|
width += 2;
|
|
@@ -152,43 +153,37 @@ export function renderDashboard(options: {
|
|
|
152
153
|
console.log('\n');
|
|
153
154
|
|
|
154
155
|
// Print Top Row: Clock aligned right
|
|
155
|
-
console.log(' '.repeat(
|
|
156
|
-
|
|
157
|
-
//
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
`${darkBorder('│')} ${yellow('MEDIUM')} ${String(stats.medium).padStart(2, ' ')} ${darkBorder('│')}`,
|
|
175
|
-
`${darkBorder('│')} ${riskColor.bold(stats.riskLevel.padEnd(13, ' '))} ${cyan('LOW')} ${String(stats.low).padStart(2, ' ')} ${darkBorder('│')}`,
|
|
176
|
-
`${darkBorder('└───────────────────────────────────────┘')}`
|
|
177
|
-
];
|
|
156
|
+
console.log(' '.repeat(65) + cyan.bold(timeStr));
|
|
157
|
+
|
|
158
|
+
// 1. Header (Shield + VIBEGUARD)
|
|
159
|
+
console.log(`${cyan(shield[0])} ${cyan.bold(title[0])}`);
|
|
160
|
+
console.log(`${cyan(shield[1])} ${cyan.bold(title[1])}`);
|
|
161
|
+
console.log(`${cyan(shield[2])} ${cyan.bold(title[2])}`);
|
|
162
|
+
console.log(`${cyan(shield[3])} ${cyan.bold(title[3])}`);
|
|
163
|
+
console.log(`${cyan(shield[4])} ${cyan.bold(title[4])}`);
|
|
164
|
+
console.log(`${cyan(shield[5])} ${gray('AI-Powered DevSecOps Orchestrator')}`);
|
|
165
|
+
console.log(` ${cyan('Scanning. Analyzing. Protecting.')}\n`);
|
|
166
|
+
|
|
167
|
+
// 2. Risk Score Box (Placed cleanly BELOW VIBEGUARD Header)
|
|
168
|
+
const boxWidth = 74;
|
|
169
|
+
console.log(darkBorder(`┌${'─'.repeat(boxWidth)}┐`));
|
|
170
|
+
console.log(`${darkBorder('│')} ${cyan.bold('OVERALL RISK SCORE')}${' '.repeat(boxWidth - 20)}${darkBorder('│')}`);
|
|
171
|
+
console.log(`${darkBorder('│')}${' '.repeat(boxWidth)}${darkBorder('│')}`);
|
|
172
|
+
|
|
173
|
+
const scoreLine1 = ` ${scoreColor.bold(String(stats.score))} ${gray('/100')} ${red('CRITICAL')} ${String(stats.critical).padEnd(2, ' ')} ${orange('HIGH')} ${String(stats.high).padEnd(2, ' ')}`;
|
|
174
|
+
const scoreLine2 = ` ${riskColor.bold(stats.riskLevel.padEnd(13, ' '))} ${yellow('MEDIUM')} ${String(stats.medium).padEnd(2, ' ')} ${cyan('LOW')} ${String(stats.low).padEnd(2, ' ')}`;
|
|
178
175
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
console.log(`${l}${r}`);
|
|
183
|
-
}
|
|
176
|
+
console.log(`${darkBorder('│')}${padVisible(scoreLine1, boxWidth)}${darkBorder('│')}`);
|
|
177
|
+
console.log(`${darkBorder('│')}${padVisible(scoreLine2, boxWidth)}${darkBorder('│')}`);
|
|
178
|
+
console.log(darkBorder(`└${'─'.repeat(boxWidth)}┘`));
|
|
184
179
|
console.log('');
|
|
185
180
|
|
|
186
|
-
// 3
|
|
181
|
+
// 3. Multi-panel Grid (Pipeline & Findings)
|
|
187
182
|
const pipelineRows = [
|
|
188
|
-
|
|
183
|
+
`</> SAST (Semgrep) ${green('✓ OK')}`,
|
|
189
184
|
`📦 Dependency Check (Trivy) ${green('✓ OK')}`,
|
|
190
185
|
`🔑 Secrets Scan (Gitleaks) ${green('✓ OK')}`,
|
|
191
|
-
|
|
186
|
+
`🔒 IaC Scan (Checkov) ${green('✓ OK')}`,
|
|
192
187
|
`🐳 Container Scan (Trivy) ${green('✓ OK')}`,
|
|
193
188
|
`📑 Code Quality (ESLint) ${green('✓ OK')}`,
|
|
194
189
|
``,
|
|
@@ -202,7 +197,7 @@ export function renderDashboard(options: {
|
|
|
202
197
|
|
|
203
198
|
// Table of findings (top 10)
|
|
204
199
|
const topFindings = findings.slice(0, 10);
|
|
205
|
-
const tableHeader = `${dimGray(padVisible('ID',
|
|
200
|
+
const tableHeader = `${dimGray(padVisible('ID', 13))} ${dimGray(padVisible('SEVERITY', 10))} ${dimGray(padVisible('TITLE', 28))} ${dimGray('FILE:LINE')}`;
|
|
206
201
|
|
|
207
202
|
const findingRows: string[] = [tableHeader];
|
|
208
203
|
|
|
@@ -218,11 +213,11 @@ export function renderDashboard(options: {
|
|
|
218
213
|
else if (sev === 'MEDIUM') sevFormatted = yellow('MEDIUM ');
|
|
219
214
|
|
|
220
215
|
const rawTitle = f.title || 'Security Finding';
|
|
221
|
-
const titleTruncated = rawTitle.length >
|
|
216
|
+
const titleTruncated = rawTitle.length > 26 ? rawTitle.slice(0, 24) + '..' : rawTitle;
|
|
222
217
|
const titleFormatted = padVisible(white(titleTruncated), 28);
|
|
223
218
|
|
|
224
219
|
const fileLine = `${f.file || 'unknown'}:${f.line || 1}`;
|
|
225
|
-
const idFormatted = padVisible(cyan(id),
|
|
220
|
+
const idFormatted = padVisible(cyan(id), 13);
|
|
226
221
|
findingRows.push(`${idFormatted} ${sevFormatted} ${titleFormatted} ${dimGray(fileLine)}`);
|
|
227
222
|
}
|
|
228
223
|
}
|
|
@@ -230,7 +225,7 @@ export function renderDashboard(options: {
|
|
|
230
225
|
// Print Section Headers with ANSI-safe padding
|
|
231
226
|
const headerCol1 = padVisible(cyan('▶ SCANNER PIPELINE'), 36);
|
|
232
227
|
const headerCol2 = cyan('▶ TOP FINDINGS');
|
|
233
|
-
console.log(
|
|
228
|
+
console.log(`${headerCol1} ${headerCol2}`);
|
|
234
229
|
|
|
235
230
|
const maxRows = Math.max(pipelineRows.length, findingRows.length);
|
|
236
231
|
for (let i = 0; i < maxRows; i++) {
|
|
@@ -239,7 +234,7 @@ export function renderDashboard(options: {
|
|
|
239
234
|
console.log(`${left} ${right}`);
|
|
240
235
|
}
|
|
241
236
|
|
|
242
|
-
// AI Remediation Section
|
|
237
|
+
// 4. AI Remediation Section
|
|
243
238
|
if (remediation) {
|
|
244
239
|
console.log(`\n${cyan('▶ AI REMEDIATION (VG-AI)')}\n`);
|
|
245
240
|
console.log(`${cyan('Finding:')} ${red.bold(remediation.findingId)}\n`);
|
|
@@ -265,7 +260,7 @@ export function renderDashboard(options: {
|
|
|
265
260
|
console.log(`\n${cyan('Confidence:')} ${green.bold(remediation.confidence + '%')}`);
|
|
266
261
|
}
|
|
267
262
|
|
|
268
|
-
// Summary Bar (bottom capsule)
|
|
263
|
+
// 5. Summary Bar (bottom capsule)
|
|
269
264
|
console.log(`\n${cyan('▶ SUMMARY')}`);
|
|
270
265
|
const summaryText = `🛡️ Scan completed in ${duration} │ ${stats.total} findings │ 6 passed`;
|
|
271
266
|
const barTop = `┌${'─'.repeat(visibleWidth(summaryText) + 4)}┐`;
|
package/src/index.ts
CHANGED
|
@@ -21,24 +21,40 @@ program
|
|
|
21
21
|
.description('Run a security scan on the current directory or target path')
|
|
22
22
|
.option('-d, --dir <path>', 'Directory to scan', process.cwd())
|
|
23
23
|
.option('--fix', 'Automatically generate AI remediation fixes and interactive diff')
|
|
24
|
-
.option('--
|
|
24
|
+
.option('--ci', 'Run in non-interactive CI mode and exit with policy status code')
|
|
25
25
|
.action(async (targetPath, options) => {
|
|
26
26
|
const scanDir = targetPath || options.dir || process.cwd();
|
|
27
27
|
const startTime = Date.now();
|
|
28
|
+
let hasSystemError = false;
|
|
28
29
|
|
|
30
|
+
// Use a minimal spinner if in CI mode, or bypass ora entirely if preferred.
|
|
31
|
+
// For simplicity, we just won't render the interactive dashboard if --ci is set.
|
|
29
32
|
const spinner = ora({
|
|
30
33
|
text: chalk.hex('#00E5FF')('Scanning repository for vulnerabilities (SAST, SCA, Secrets, IaC)...'),
|
|
31
|
-
spinner: 'dots'
|
|
34
|
+
spinner: 'dots',
|
|
35
|
+
isSilent: options.ci
|
|
32
36
|
}).start();
|
|
33
37
|
|
|
34
38
|
let findings: NormalizedFinding[] = [];
|
|
35
39
|
|
|
36
40
|
try {
|
|
37
|
-
// Initialize the security orchestrator and
|
|
41
|
+
// Initialize the security orchestrator and scanners
|
|
38
42
|
const { Orchestrator } = require('@maverick006/security-engine');
|
|
39
43
|
const { NpmAuditScanner } = require('@maverick006/scanner-npm-audit');
|
|
40
|
-
|
|
41
|
-
const
|
|
44
|
+
const { TrivyScanner } = require('@maverick006/scanner-trivy');
|
|
45
|
+
const { SemgrepScanner } = require('@maverick006/scanner-semgrep');
|
|
46
|
+
const { GitleaksScanner } = require('@maverick006/scanner-gitleaks');
|
|
47
|
+
const { CheckovScanner } = require('@maverick006/scanner-checkov');
|
|
48
|
+
const { ZapScanner } = require('@maverick006/scanner-zap');
|
|
49
|
+
|
|
50
|
+
const orchestrator = new Orchestrator([
|
|
51
|
+
new NpmAuditScanner(),
|
|
52
|
+
new TrivyScanner(),
|
|
53
|
+
new SemgrepScanner(),
|
|
54
|
+
new GitleaksScanner(),
|
|
55
|
+
new CheckovScanner(),
|
|
56
|
+
new ZapScanner()
|
|
57
|
+
]);
|
|
42
58
|
|
|
43
59
|
const scanResult = await orchestrator.runScan({
|
|
44
60
|
scanId: `scan-${Date.now()}`,
|
|
@@ -49,109 +65,8 @@ program
|
|
|
49
65
|
|
|
50
66
|
findings = scanResult.findings || [];
|
|
51
67
|
} catch (err) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
// If demo mode or no findings found in empty directory, provide rich showcase findings matching design
|
|
56
|
-
if (options.demo || findings.length === 0) {
|
|
57
|
-
findings = [
|
|
58
|
-
{
|
|
59
|
-
id: 'VG-CRIT-001',
|
|
60
|
-
title: 'Hardcoded AWS Secret Key',
|
|
61
|
-
severity: Severity.CRITICAL,
|
|
62
|
-
scanner: 'Gitleaks',
|
|
63
|
-
file: 'config/aws.py',
|
|
64
|
-
line: 12,
|
|
65
|
-
description: 'AWS Secret Key is hardcoded in the source code.',
|
|
66
|
-
codeSnippet: 'AWS_SECRET_KEY = "AKIAIOSFODNN7EXAMPLE"'
|
|
67
|
-
},
|
|
68
|
-
{
|
|
69
|
-
id: 'VG-CRIT-002',
|
|
70
|
-
title: 'Exposed JWT Secret',
|
|
71
|
-
severity: Severity.CRITICAL,
|
|
72
|
-
scanner: 'Gitleaks',
|
|
73
|
-
file: 'config/auth.py',
|
|
74
|
-
line: 8,
|
|
75
|
-
description: 'Hardcoded JWT secret token detected.',
|
|
76
|
-
codeSnippet: 'const JWT_SECRET = "super_secret_123"'
|
|
77
|
-
},
|
|
78
|
-
{
|
|
79
|
-
id: 'VG-HIGH-003',
|
|
80
|
-
title: 'SQL Injection Risk',
|
|
81
|
-
severity: Severity.HIGH,
|
|
82
|
-
scanner: 'Semgrep',
|
|
83
|
-
file: 'api/users.py',
|
|
84
|
-
line: 45,
|
|
85
|
-
description: 'Direct concatenation of user input into SQL query.',
|
|
86
|
-
codeSnippet: 'query = "SELECT * FROM users WHERE name = " + req.query.name'
|
|
87
|
-
},
|
|
88
|
-
{
|
|
89
|
-
id: 'VG-HIGH-004',
|
|
90
|
-
title: 'Outdated Dependency (lodash)',
|
|
91
|
-
severity: Severity.HIGH,
|
|
92
|
-
scanner: 'npm-audit',
|
|
93
|
-
file: 'package.json',
|
|
94
|
-
line: 23,
|
|
95
|
-
description: 'Vulnerable prototype pollution in lodash version.',
|
|
96
|
-
codeSnippet: '"lodash": "4.17.15"'
|
|
97
|
-
},
|
|
98
|
-
{
|
|
99
|
-
id: 'VG-HIGH-005',
|
|
100
|
-
title: 'Unsafe Deserialization',
|
|
101
|
-
severity: Severity.HIGH,
|
|
102
|
-
scanner: 'Semgrep',
|
|
103
|
-
file: 'utils/parser.py',
|
|
104
|
-
line: 78,
|
|
105
|
-
description: 'Unsafe pickle.loads execution.',
|
|
106
|
-
codeSnippet: 'data = pickle.loads(user_input)'
|
|
107
|
-
},
|
|
108
|
-
{
|
|
109
|
-
id: 'VG-MED-006',
|
|
110
|
-
title: 'S3 Bucket Public Read',
|
|
111
|
-
severity: Severity.MEDIUM,
|
|
112
|
-
scanner: 'Checkov',
|
|
113
|
-
file: 'iac/s3.tf',
|
|
114
|
-
line: 14,
|
|
115
|
-
description: 'Public read access enabled on production storage bucket.',
|
|
116
|
-
codeSnippet: 'acl = "public-read"'
|
|
117
|
-
},
|
|
118
|
-
{
|
|
119
|
-
id: 'VG-MED-007',
|
|
120
|
-
title: 'Missing Rate Limiting',
|
|
121
|
-
severity: Severity.MEDIUM,
|
|
122
|
-
scanner: 'Semgrep',
|
|
123
|
-
file: 'api/auth.py',
|
|
124
|
-
line: 32,
|
|
125
|
-
description: 'Authentication endpoint lacks rate limiting protection.'
|
|
126
|
-
},
|
|
127
|
-
{
|
|
128
|
-
id: 'VG-MED-008',
|
|
129
|
-
title: 'CORS Misconfiguration',
|
|
130
|
-
severity: Severity.MEDIUM,
|
|
131
|
-
scanner: 'Semgrep',
|
|
132
|
-
file: 'web/middleware.py',
|
|
133
|
-
line: 19,
|
|
134
|
-
description: 'Wildcard CORS origin enabled in production middleware.'
|
|
135
|
-
},
|
|
136
|
-
{
|
|
137
|
-
id: 'VG-LOW-009',
|
|
138
|
-
title: 'Unused Dependency',
|
|
139
|
-
severity: Severity.LOW,
|
|
140
|
-
scanner: 'npm-audit',
|
|
141
|
-
file: 'package.json',
|
|
142
|
-
line: 102,
|
|
143
|
-
description: 'Unused package detected.'
|
|
144
|
-
},
|
|
145
|
-
{
|
|
146
|
-
id: 'VG-LOW-010',
|
|
147
|
-
title: 'Missing Security Headers',
|
|
148
|
-
severity: Severity.LOW,
|
|
149
|
-
scanner: 'Zap',
|
|
150
|
-
file: 'web/server.py',
|
|
151
|
-
line: 56,
|
|
152
|
-
description: 'Strict-Transport-Security header is not set.'
|
|
153
|
-
}
|
|
154
|
-
];
|
|
68
|
+
console.error(chalk.red('Orchestrator failed to run scans.'), err);
|
|
69
|
+
hasSystemError = true;
|
|
155
70
|
}
|
|
156
71
|
|
|
157
72
|
spinner.stop();
|
|
@@ -166,13 +81,13 @@ program
|
|
|
166
81
|
|
|
167
82
|
let remediationData: any = undefined;
|
|
168
83
|
|
|
169
|
-
// Generate AI Remediation
|
|
170
|
-
if (options.fix || true) {
|
|
84
|
+
// Generate AI Remediation only if not in CI mode to save time, or if explicitly asked
|
|
85
|
+
if (findings.length > 0 && !options.ci && (options.fix || true)) {
|
|
171
86
|
try {
|
|
172
87
|
const explainer = new ContextualExplainer();
|
|
173
88
|
const primaryFinding = findings[0];
|
|
174
89
|
|
|
175
|
-
let snippet = primaryFinding.codeSnippet || '
|
|
90
|
+
let snippet = primaryFinding.codeSnippet || '';
|
|
176
91
|
if (primaryFinding.file && existsSync(join(scanDir, primaryFinding.file))) {
|
|
177
92
|
const content = readFileSync(join(scanDir, primaryFinding.file), 'utf-8');
|
|
178
93
|
const lines = content.split('\n');
|
|
@@ -182,46 +97,92 @@ program
|
|
|
182
97
|
|
|
183
98
|
const explanation = await explainer.explainFinding(primaryFinding, { codeContext: snippet });
|
|
184
99
|
|
|
185
|
-
const diffSnippet = [
|
|
186
|
-
'10 # config/aws.py',
|
|
187
|
-
'- 11 AWS_SECRET_KEY = "AKIAIOSFODNN7EXAMPLE"',
|
|
188
|
-
'+ 12 AWS_SECRET_KEY = os.getenv("AWS_SECRET_KEY")',
|
|
189
|
-
'13'
|
|
190
|
-
].join('\n');
|
|
191
|
-
|
|
192
100
|
remediationData = {
|
|
193
|
-
findingId: primaryFinding.id
|
|
194
|
-
issue: explanation.summary
|
|
195
|
-
impact:
|
|
196
|
-
recommendation: explanation.remediation
|
|
197
|
-
diffSnippet: explanation.codeFix
|
|
198
|
-
confidence:
|
|
101
|
+
findingId: primaryFinding.id,
|
|
102
|
+
issue: explanation.summary,
|
|
103
|
+
impact: explanation.details || 'Potential security impact based on context.',
|
|
104
|
+
recommendation: explanation.remediation,
|
|
105
|
+
diffSnippet: explanation.codeFix || '',
|
|
106
|
+
confidence: 90
|
|
199
107
|
};
|
|
200
108
|
} catch (e: any) {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
impact: 'This can lead to unauthorized access to your AWS resources.',
|
|
205
|
-
recommendation: 'Use environment variables or AWS Secrets Manager to store secrets.',
|
|
206
|
-
diffSnippet: [
|
|
207
|
-
'10 # config/aws.py',
|
|
208
|
-
'- 11 AWS_SECRET_KEY = "AKIAIOSFODNN7EXAMPLE"',
|
|
209
|
-
'+ 12 AWS_SECRET_KEY = os.getenv("AWS_SECRET_KEY")',
|
|
210
|
-
'13'
|
|
211
|
-
].join('\n'),
|
|
212
|
-
confidence: 98
|
|
213
|
-
};
|
|
109
|
+
if (!options.ci) {
|
|
110
|
+
console.error(chalk.yellow('AI Remediation generation failed.'), e);
|
|
111
|
+
}
|
|
214
112
|
}
|
|
215
113
|
}
|
|
216
114
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
115
|
+
if (!options.ci) {
|
|
116
|
+
// Render the beautiful cyber dashboard interactively
|
|
117
|
+
renderDashboard({
|
|
118
|
+
findings,
|
|
119
|
+
stats,
|
|
120
|
+
gitInfo,
|
|
121
|
+
duration,
|
|
122
|
+
remediation: remediationData
|
|
123
|
+
});
|
|
124
|
+
} else {
|
|
125
|
+
// CI Output
|
|
126
|
+
console.log(`VibeGuard CI Scan Complete. Duration: ${duration}`);
|
|
127
|
+
console.log(`Findings: ${findings.length}`);
|
|
128
|
+
console.log(`Risk Score: ${stats.score}/100 (${stats.riskLevel})`);
|
|
129
|
+
const critical = findings.filter(f => (f.severity || '').toUpperCase() === Severity.CRITICAL).length;
|
|
130
|
+
const high = findings.filter(f => (f.severity || '').toUpperCase() === Severity.HIGH).length;
|
|
131
|
+
console.log(`Critical: ${critical}, High: ${high}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Sync with Web Dashboard
|
|
135
|
+
const syncSpinner = ora({
|
|
136
|
+
text: chalk.dim('Syncing results to VibeGuard Dashboard...'),
|
|
137
|
+
spinner: 'dots',
|
|
138
|
+
isSilent: options.ci
|
|
139
|
+
}).start();
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
const API_URL = process.env.VIBEGUARD_API_URL || 'http://localhost:3001';
|
|
143
|
+
const API_KEY = process.env.VIBEGUARD_API_KEY;
|
|
144
|
+
const response = await fetch(`${API_URL}/api/scans/upload`, {
|
|
145
|
+
method: 'POST',
|
|
146
|
+
headers: {
|
|
147
|
+
'Content-Type': 'application/json',
|
|
148
|
+
'Authorization': `Bearer ${API_KEY}`
|
|
149
|
+
},
|
|
150
|
+
body: JSON.stringify({
|
|
151
|
+
repositoryName: gitInfo.name || 'Local Project',
|
|
152
|
+
repositoryUrl: gitInfo.name || 'local',
|
|
153
|
+
numericScore: stats.score,
|
|
154
|
+
score: stats.riskLevel,
|
|
155
|
+
findings: findings
|
|
156
|
+
})
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
if (response.ok) {
|
|
160
|
+
syncSpinner.succeed(chalk.dim('Results synced to dashboard.'));
|
|
161
|
+
} else {
|
|
162
|
+
syncSpinner.fail(chalk.dim('Failed to sync results to dashboard.'));
|
|
163
|
+
}
|
|
164
|
+
} catch (e) {
|
|
165
|
+
syncSpinner.warn(chalk.dim('Dashboard API unreachable. Skipping sync.'));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Exit codes
|
|
169
|
+
if (hasSystemError) {
|
|
170
|
+
process.exit(2);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Policy fail if we have CRITICAL or HIGH findings
|
|
174
|
+
const criticalOrHighCount = findings.filter(f => {
|
|
175
|
+
const s = (f.severity || '').toUpperCase();
|
|
176
|
+
return s === Severity.CRITICAL || s === Severity.HIGH;
|
|
177
|
+
}).length;
|
|
178
|
+
|
|
179
|
+
if (criticalOrHighCount > 0) {
|
|
180
|
+
if (options.ci) console.error(chalk.red('Security Policy FAILED.'));
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (options.ci) console.log(chalk.green('Security Policy PASSED.'));
|
|
185
|
+
process.exit(0);
|
|
225
186
|
});
|
|
226
187
|
|
|
227
188
|
program.parse(process.argv);
|