vibe-audit-scan 1.2.9

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/README.md ADDED
@@ -0,0 +1,87 @@
1
+ # Vibe Audit CLI
2
+
3
+ Vibe Audit is a production-grade security scanner for modern codebases. It analyzes your application for vulnerabilities, architectural flaws, and business logic risks, then delivers AI-powered remediation guides so your team can ship with confidence.
4
+
5
+ Vibe Audit is not a linter. It does not flag formatting issues, naming conventions, or stylistic preferences. It exists for one reason: to catch the security flaws, architectural blind spots, and business logic oversights that silently break applications in production.
6
+
7
+ Where traditional static analysis tools stop at syntax, Vibe Audit goes deeper. It scans your codebase for the vulnerabilities that attackers actually exploit: authentication bypasses, broken access control, payment logic gaps, missing rate limits, unsafe data deletion, N+1 query bottlenecks, and leaked secrets. Every finding is graded by production impact (Critical, High, or Warning) and paired with specific, actionable remediation steps so your team knows exactly what to fix and how.
8
+
9
+ ## How It Works
10
+ The Vibe Audit CLI scans your codebase using the built-in Core scanning engine, then sends the results to the Vibe Audit platform where an AI-powered analysis pipeline produces context-aware, developer-ready remediation guides for every finding.
11
+
12
+ ## Scanner Installation
13
+ The CLI automatically downloads and manages the platform-specific core scan engine binary on first run. Binaries are stored locally within the user home directory (~/.vibe-audit-scan/bin/), ensuring zero-config initial setup.
14
+
15
+ ## Getting Started
16
+ To use the Vibe Audit CLI, you must first create an account on the platform:
17
+ 1. Visit http://www.vibeauditscan.com to sign up for an account.
18
+ 2. Run the login command to authenticate the CLI with your credentials.
19
+
20
+ ## Prerequisites
21
+ - Node.js 22+
22
+
23
+ ## Installation
24
+ Once published to npm, you can install the utility globally:
25
+ ```bash
26
+ npm install -g vibe-audit-scan
27
+ ```
28
+
29
+ For local development setup:
30
+ ```bash
31
+ # Navigate to the CLI package directory
32
+ cd packages/cli
33
+
34
+ # Link local CLI binary for testing
35
+ npm link
36
+ ```
37
+
38
+ ## Usage
39
+
40
+ ### Run codebase scans
41
+ Before running a scan, ensure you are authenticated.
42
+ ```bash
43
+ # Scan current directory
44
+ vibe scan
45
+
46
+ # Scan specific directory
47
+ vibe scan /path/to/your/project
48
+
49
+ # Save results to a local file
50
+ vibe scan /path/to/your/project -o results.json
51
+ ```
52
+
53
+ ### Authenticate with Vibe Cloud
54
+ ```bash
55
+ # Log in via your web browser
56
+ vibe login
57
+
58
+ # Check active session profile
59
+ vibe whoami
60
+
61
+ # Log out and revoke active tokens
62
+ vibe logout
63
+ ```
64
+
65
+ ## What Vibe Audit Looks For
66
+ - Authentication and authorization bypasses
67
+ - Insecure Direct Object References (IDOR) and Broken Object-Level Authorization (BOLA)
68
+ - Payment, billing, and subscription logic flaws
69
+ - Missing or weak rate limiting on critical endpoints
70
+ - Leaked credentials, secrets, and API keys
71
+ - Unsafe data deletion and missing cascade protections
72
+ - N+1 query patterns and database performance risks
73
+ - SQL injection, XSS, SSRF, and path traversal
74
+ - Race conditions, unhandled edge cases, and memory leaks
75
+
76
+ ## What Vibe Audit Does Not Look For
77
+ - Code formatting and style preferences
78
+ - Naming conventions or variable casing
79
+ - Unused imports or dead code
80
+ - Indentation, whitespace, or line length
81
+ - Any issue that would be caught by ESLint, Prettier, or similar linting tools
82
+
83
+ ## Features
84
+ - Zero Configuration: The installer automatically bootstraps its own scanning binaries.
85
+ - Production-First Analysis: Ignores noise. Surfaces only the flaws that threaten your application in production.
86
+ - Severity Grading: Every finding is classified as Critical, High, or Warning with clear remediation paths.
87
+ - AI-Powered Remediation: Automatically triggers the Vibe AI worker for detailed, developer-ready fix guides.
@@ -0,0 +1,109 @@
1
+ #! /usr/bin/env node
2
+ import { program } from 'commander';
3
+ import chalk from 'chalk';
4
+ import boxen from 'boxen';
5
+ import { scan } from '../src/commands/scan.js';
6
+ import { login, logout, whoami } from '../src/commands/auth.js';
7
+ import { printWelcome, printHelpCheatsheet } from '../src/utils/welcome.js';
8
+
9
+ program
10
+ .name('vibe')
11
+ .version('1.2.6', '-V, --version', 'output the version number')
12
+ .description('Vibe Audit CLI - Scan your codebase for vulnerabilities')
13
+ .configureOutput({
14
+ writeOut: (str) => {
15
+ // Check if this is the version output
16
+ if (str.trim().match(/^\d+\.\d+\.\d+$/)) {
17
+ console.log(chalk.cyan.bold('vibe-audit-scan v' + str.trim()));
18
+ } else {
19
+ console.log(boxen(str, {
20
+ padding: 1,
21
+ margin: 1,
22
+ borderStyle: 'round',
23
+ borderColor: 'cyan'
24
+ }));
25
+ }
26
+ },
27
+ writeErr: (str) => {
28
+ let msg = str.trim();
29
+ if (msg.includes('unknown command') || msg.includes('invalid command')) {
30
+ msg = `❌ Command does not exist.\n\nPlease run "${chalk.cyan('vibe cheatsheet')}" to see the list of available commands.`;
31
+ }
32
+ console.log(boxen(msg, {
33
+ padding: 1,
34
+ margin: 1,
35
+ borderStyle: 'round',
36
+ borderColor: 'red'
37
+ }));
38
+ },
39
+ outputError: (str, write) => write(chalk.red(str))
40
+ });
41
+
42
+ program
43
+ .command('cheatsheet')
44
+ .description('List shortcuts and status')
45
+ .action(() => {
46
+ printHelpCheatsheet();
47
+ });
48
+
49
+ program
50
+ .command('login')
51
+ .description('Authenticate your CLI with the Vibe Cloud backend')
52
+ .option('--api-url <url>', 'API endpoint to use for authentication')
53
+ .option('--web-url <url>', 'URL of the Vibe Audit web app to open for OAuth')
54
+ .action(async (options) => {
55
+ try {
56
+ await login(options);
57
+ } catch (error) {
58
+ console.error('Error:', error.message);
59
+ process.exit(1);
60
+ }
61
+ });
62
+
63
+ program
64
+ .command('logout')
65
+ .description('Revokes your session and clears local tokens')
66
+ .action(async () => {
67
+ try {
68
+ await logout();
69
+ } catch (error) {
70
+ console.error('Error:', error.message);
71
+ process.exit(1);
72
+ }
73
+ });
74
+
75
+ program
76
+ .command('whoami')
77
+ .description('Displays the currently logged-in user profile')
78
+ .option('--api-url <url>', 'API endpoint to use to verify profile')
79
+ .action(async (options) => {
80
+ try {
81
+ await whoami(options);
82
+ } catch (error) {
83
+ console.error('Error:', error.message);
84
+ process.exit(1);
85
+ }
86
+ });
87
+
88
+ program
89
+ .command('scan')
90
+ .description('Scan a directory for vulnerabilities')
91
+ .argument('[directory]', 'Directory to scan', '.')
92
+ .option('-o, --output <file>', 'Output JSON file')
93
+ .option('--api-url <url>', 'API endpoint to send scan data to')
94
+ .action(async (directory, options) => {
95
+ try {
96
+ await scan(directory, options);
97
+ } catch (error) {
98
+ console.error('Error:', error.message);
99
+ process.exit(1);
100
+ }
101
+ });
102
+
103
+ // Handle vibe without command - show welcome screen
104
+ if (process.argv.length <= 2) {
105
+ printWelcome();
106
+ process.exit(0);
107
+ }
108
+
109
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "vibe-audit-scan",
3
+ "version": "1.2.9",
4
+ "type": "module",
5
+ "license": "UNLICENSED",
6
+ "author": "Vibe Audit",
7
+ "copyright": "Copyright (c) 2026 Vibe Audit. All Rights Reserved.",
8
+ "bin": {
9
+ "vibe": "./bin/vibe-audit.js",
10
+ "vibe-audit-scan": "./bin/vibe-audit.js"
11
+ },
12
+ "scripts": {
13
+ "postinstall": "node -e \"console.log('\\n\\n🎉 Vibe Audit CLI installed successfully!\\n\\n🚀 Run \\\"vibe scan\\\" inside your project folder to start scanning.\\n\\n')\""
14
+ },
15
+ "dependencies": {
16
+ "adm-zip": "^0.5.17",
17
+ "axios": "^1.17.0",
18
+ "boxen": "^8.0.1",
19
+ "chalk": "^5.6.2",
20
+ "commander": "^13.1.0",
21
+ "fs-extra": "^11.3.5",
22
+ "tar": "^7.5.16"
23
+ }
24
+ }
@@ -0,0 +1,226 @@
1
+ import http from 'http';
2
+ import { exec } from 'child_process';
3
+ import axios from 'axios';
4
+ import chalk from 'chalk';
5
+ import boxen from 'boxen';
6
+ import { setToken, clearToken, getToken, getApiUrl, getWebUrl } from '../utils/config.js';
7
+
8
+ function getFreePort(startPort) {
9
+ return new Promise((resolve, reject) => {
10
+ const server = http.createServer();
11
+ server.on('error', (err) => {
12
+ if (err.code === 'EADDRINUSE') {
13
+ resolve(getFreePort(startPort + 1));
14
+ } else {
15
+ reject(err);
16
+ }
17
+ });
18
+ server.listen(startPort, () => {
19
+ const { port } = server.address();
20
+ server.close(() => resolve(port));
21
+ });
22
+ });
23
+ }
24
+
25
+ export async function login(options) {
26
+ try {
27
+ const webUrl = getWebUrl(options.webUrl);
28
+ const port = await getFreePort(9999);
29
+ const authUrl = `${webUrl}/cli-login?port=${port}`;
30
+
31
+ console.log(chalk.cyan(`\n🔑 Initializing browser-based login...`));
32
+ console.log(chalk.gray(`Opening your default browser to: ${authUrl}\n`));
33
+
34
+ // Open default browser depending on platform
35
+ const openCmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
36
+ exec(`${openCmd} "${authUrl}"`, (err) => {
37
+ if (err) {
38
+ console.log(chalk.yellow(`Could not open browser automatically. Please open this link manually:\n ${chalk.underline(authUrl)}`));
39
+ }
40
+ });
41
+
42
+ return new Promise((resolve) => {
43
+ const server = http.createServer((req, res) => {
44
+ const parsedUrl = new URL(req.url, `http://${req.headers.host}`);
45
+
46
+ if (parsedUrl.pathname === '/callback') {
47
+ const token = parsedUrl.searchParams.get('token');
48
+ const email = parsedUrl.searchParams.get('email');
49
+
50
+ if (token) {
51
+ const escapedEmail = (email || '')
52
+ .replace(/&/g, '&amp;')
53
+ .replace(/</g, '&lt;')
54
+ .replace(/>/g, '&gt;')
55
+ .replace(/"/g, '&quot;')
56
+ .replace(/'/g, '&#039;');
57
+
58
+ setToken(token, escapedEmail);
59
+
60
+ res.writeHead(200, { 'Content-Type': 'text/html' });
61
+ res.end(`
62
+ <!DOCTYPE html>
63
+ <html>
64
+ <head>
65
+ <title>Success | Vibe Audit CLI</title>
66
+ <style>
67
+ body {
68
+ background-color: #09090b;
69
+ color: #fafafa;
70
+ font-family: ui-sans-serif, system-ui, sans-serif;
71
+ display: flex;
72
+ flex-direction: column;
73
+ align-items: center;
74
+ justify-content: center;
75
+ height: 100vh;
76
+ margin: 0;
77
+ text-align: center;
78
+ overflow: hidden;
79
+ }
80
+ .logo-container {
81
+ display: flex;
82
+ flex-direction: column;
83
+ align-items: center;
84
+ gap: 0.75rem;
85
+ margin-bottom: 1.5rem;
86
+ }
87
+ .logo {
88
+ width: 5rem;
89
+ height: 5rem;
90
+ color: #22d3ee;
91
+ filter: drop-shadow(0 0 20px rgba(34, 211, 238, 0.35));
92
+ }
93
+ .brand {
94
+ font-size: 2rem;
95
+ font-weight: 950;
96
+ letter-spacing: -0.075em;
97
+ color: #ffffff;
98
+ margin-top: 0.25rem;
99
+ }
100
+ .brand-sub {
101
+ color: #71717a;
102
+ }
103
+ h1 {
104
+ color: #22d3ee;
105
+ margin: 1rem 0 0.5rem 0;
106
+ font-size: 2.25rem;
107
+ font-weight: 900;
108
+ letter-spacing: -0.03em;
109
+ }
110
+ p {
111
+ color: #71717a;
112
+ font-size: 0.95rem;
113
+ line-height: 1.6;
114
+ max-width: 26rem;
115
+ margin: 0;
116
+ }
117
+ </style>
118
+ </head>
119
+ <body>
120
+ <div class="logo-container">
121
+ <svg class="logo" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
122
+ <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
123
+ <path d="M12 8v4"/>
124
+ <path d="M12 16h.01"/>
125
+ </svg>
126
+ <div class="brand">VIBE<span class="brand-sub">AUDIT</span></div>
127
+ </div>
128
+ <h1>Authorization Successful</h1>
129
+ <p>You have successfully logged in to Vibe Audit CLI.<br>You can close this tab and return to the terminal.</p>
130
+ </body>
131
+ </html>
132
+ `);
133
+
134
+ console.log(boxen(`
135
+ ${chalk.green.bold('🎉 Authentication Successful!')}
136
+ Logged in as: ${chalk.white(escapedEmail || 'User')}
137
+ `, {
138
+ padding: 1,
139
+ borderStyle: 'round',
140
+ borderColor: 'green'
141
+ }));
142
+
143
+ // Close server and complete promise
144
+ res.socket.destroy();
145
+ server.close(() => {
146
+ resolve(true);
147
+ });
148
+ } else {
149
+ res.writeHead(400, { 'Content-Type': 'text/html' });
150
+ res.end(`
151
+ <!DOCTYPE html>
152
+ <html>
153
+ <body>
154
+ <h1 style="color: red;">Authorization Failed</h1>
155
+ <p>No authorization token received. Please try again.</p>
156
+ </body>
157
+ </html>
158
+ `);
159
+ console.log(chalk.red('\n❌ Authorization failed: No token received.'));
160
+ res.socket.destroy();
161
+ server.close(() => {
162
+ resolve(false);
163
+ });
164
+ }
165
+ } else {
166
+ res.writeHead(404);
167
+ res.end();
168
+ }
169
+ });
170
+
171
+ server.listen(port);
172
+ });
173
+ } catch (error) {
174
+ console.log(chalk.red(`\n❌ Login failed: ${error.message}`));
175
+ }
176
+ }
177
+
178
+ export async function logout() {
179
+ clearToken();
180
+ console.log(chalk.green('👋 Successfully logged out of Vibe Audit. Local tokens cleared.'));
181
+ }
182
+
183
+ export async function whoami(options) {
184
+ const token = getToken();
185
+ if (!token) {
186
+ console.log(chalk.yellow('You are not logged in. Run ') + chalk.white.bold('vibe login') + chalk.yellow(' to authenticate.'));
187
+ return;
188
+ }
189
+
190
+ const apiUrl = getApiUrl(options.apiUrl);
191
+
192
+ try {
193
+ const response = await axios.get(`${apiUrl}/api/auth/profile`, {
194
+ headers: {
195
+ Authorization: `Bearer ${token}`
196
+ }
197
+ });
198
+
199
+ if (response.data?.success && response.data?.profile) {
200
+ const profile = response.data.profile;
201
+ const profileInfo = `
202
+ ${chalk.cyan.bold('Vibe User Profile')}
203
+ ${chalk.gray('-'.repeat(30))}
204
+ Name: ${chalk.white(profile.name)}
205
+ Email: ${chalk.white(profile.email)}
206
+ Role: ${chalk.cyan(profile.role.toUpperCase())}
207
+ `;
208
+ console.log(boxen(profileInfo, {
209
+ padding: 1,
210
+ borderStyle: 'round',
211
+ borderColor: 'cyan'
212
+ }));
213
+ } else {
214
+ console.log(chalk.red('Failed to retrieve user profile details.'));
215
+ }
216
+ } catch (error) {
217
+ // If token has expired or is invalid
218
+ if (error.response?.status === 401) {
219
+ console.log(chalk.red('❌ Your session has expired or is invalid. Please run vibe login to sign in again.'));
220
+ // Auto clear bad token
221
+ clearToken();
222
+ } else {
223
+ console.log(chalk.red(`❌ Failed to retrieve profile: ${error.message}`));
224
+ }
225
+ }
226
+ }
@@ -0,0 +1,406 @@
1
+ import { execFile, exec } from 'child_process';
2
+ import util from 'util';
3
+ import fs from 'fs/promises';
4
+ import path from 'path';
5
+ import { fileURLToPath } from 'url';
6
+ import axios from 'axios';
7
+ import chalk from 'chalk';
8
+ import readline from 'readline';
9
+ import { ensureTrivyInstalled } from '../utils/trivyInstaller.js';
10
+ import { getToken, getApiUrl, getWebUrl, getDeviceId } from '../utils/config.js';
11
+
12
+ const execFilePromise = util.promisify(execFile);
13
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
14
+
15
+ export async function scan(directory, options) {
16
+ console.log(`🔍 Scanning directory: ${directory} for HIGH/CRITICAL vulnerabilities...`);
17
+
18
+ // Ensure Trivy is installed
19
+ const trivyPath = await ensureTrivyInstalled();
20
+
21
+ // Run Trivy scan (only HIGH and CRITICAL)
22
+ const scanData = await runTrivyScan(directory, trivyPath);
23
+
24
+ // Normalize secrets and misconfigurations into the Vulnerabilities array
25
+ const normalizedData = normalizeTrivyData(scanData);
26
+
27
+ // Filter to only HIGH and CRITICAL
28
+ const filteredData = filterHighCritical(normalizedData);
29
+
30
+ // Collect code files for SAST deep logic scanning
31
+ const codeFiles = await collectCodeFiles(directory);
32
+ if (codeFiles.length > 0) {
33
+ console.log(`📂 Collected ${codeFiles.length} source code files for deep logic SAST analysis.`);
34
+ }
35
+
36
+ if ((!filteredData || filteredData.Results?.length === 0) && codeFiles.length === 0) {
37
+ console.log('✅ No vulnerabilities or source files found to scan!');
38
+ return filteredData;
39
+ }
40
+
41
+ // Count vulnerabilities
42
+ let vulnCount = 0;
43
+ filteredData.Results?.forEach(r => {
44
+ vulnCount += r.Vulnerabilities?.length || 0;
45
+ });
46
+ console.log(`⚠️ Found ${vulnCount} HIGH/CRITICAL vulnerabilities!`);
47
+
48
+ // Save to file if output option is provided
49
+ if (options.output) {
50
+ const outputPath = path.resolve(options.output);
51
+ await fs.writeFile(outputPath, JSON.stringify(filteredData, null, 2));
52
+ console.log(`📄 Scan results saved to: ${outputPath}`);
53
+ }
54
+
55
+ // Send to API if token is present or apiUrl is explicitly provided
56
+ const token = getToken();
57
+ const rawApiUrl = getApiUrl(options.apiUrl);
58
+
59
+ if (token || options.apiUrl) {
60
+ const gatewayUrl = rawApiUrl.endsWith('/')
61
+ ? `${rawApiUrl}api/audit/gateway`
62
+ : `${rawApiUrl}/api/audit/gateway`;
63
+
64
+ console.log(`📡 Sending scan data to Vibe Gateway at ${gatewayUrl}...`);
65
+ try {
66
+ const result = await sendToAPI(gatewayUrl, filteredData, directory, token, codeFiles);
67
+ console.log('✅ Scan data sent successfully!');
68
+ if (result && result.jobId) {
69
+ await pollJobStatus(rawApiUrl, result.jobId, token);
70
+ }
71
+ } catch (err) {
72
+ console.error(chalk.red(`❌ Failed to send scan data: ${err.message}`));
73
+ }
74
+ } else {
75
+ console.log(chalk.yellow('\n💡 Tip: Run ') + chalk.white.bold('vibe login') + chalk.yellow(' to automatically sync reports to your Vibe Audit web dashboard.'));
76
+ }
77
+
78
+ return filteredData;
79
+ }
80
+
81
+ async function runTrivyScan(directory, trivyPath) {
82
+ try {
83
+ // Run Trivy with vuln, secret, and misconfig scanners, bypassing slow DB/policy updates and ignoring heavy directories
84
+ const { stdout } = await execFilePromise(
85
+ trivyPath,
86
+ [
87
+ 'fs',
88
+ '--format', 'json',
89
+ '--severity', 'HIGH,CRITICAL',
90
+ '--scanners', 'vuln,secret,misconfig',
91
+ '--skip-db-update',
92
+ '--skip-policy-update',
93
+ '--skip-dirs', 'node_modules,dist,build,.git,.next,.npm-cache,coverage,tmp,temp,venv,.venv',
94
+ directory
95
+ ],
96
+ { maxBuffer: 10 * 1024 * 1024 } // 10MB buffer for large outputs
97
+ );
98
+
99
+ return JSON.parse(stdout);
100
+ } catch (error) {
101
+ // Code 1 means vulnerabilities found, which is expected, so still parse output
102
+ if (error.code === 1 && error.stdout) {
103
+ return JSON.parse(error.stdout);
104
+ }
105
+ throw new Error(`Core scan engine execution failed: ${error.message}`);
106
+ }
107
+ }
108
+
109
+ async function collectCodeFiles(directory) {
110
+ const allowedExtensions = new Set(['.js', '.ts', '.tsx', '.jsx', '.py', '.go', '.rb', '.cs', '.java', '.php', '.cpp', '.c', '.swift']);
111
+ const ignoredNames = new Set([
112
+ 'node_modules', 'dist', '.git', '.next', 'build', 'bin', 'obj',
113
+ '.npm-cache', '.svelte-kit', '.vscode', '.idea', 'vendor', 'tmp',
114
+ 'temp', 'coverage', 'out', 'target', 'venv', '.venv', 'env', '.env'
115
+ ]);
116
+
117
+ const priorityDirs = ['controllers', 'routes', 'api', 'middleware', 'auth', 'models', 'src/controllers', 'src/routes', 'src/api', 'src/middleware', 'src/auth', 'src/models'];
118
+
119
+ const collected = [];
120
+ const collectedPaths = new Set();
121
+ let totalSize = 0;
122
+ const MAX_FILES = 15;
123
+ const MAX_SIZE = 150 * 1024; // 150KB limit
124
+
125
+ // Recursively find matching files
126
+ async function walk(currentDir) {
127
+ if (collected.length >= MAX_FILES || totalSize >= MAX_SIZE) return;
128
+
129
+ let entries = [];
130
+ try {
131
+ entries = await fs.readdir(currentDir, { withFileTypes: true });
132
+ } catch (e) {
133
+ return;
134
+ }
135
+
136
+ // Process subdirectories first if they match priority directories
137
+ const directories = [];
138
+ const files = [];
139
+
140
+ for (const entry of entries) {
141
+ if (entry.name.startsWith('.')) {
142
+ if (entry.name !== '.env') {
143
+ continue;
144
+ }
145
+ }
146
+ if (ignoredNames.has(entry.name.toLowerCase())) continue;
147
+
148
+ const fullPath = path.join(currentDir, entry.name);
149
+ if (entry.isDirectory()) {
150
+ directories.push(fullPath);
151
+ } else if (entry.isFile()) {
152
+ files.push({ name: entry.name, fullPath });
153
+ }
154
+ }
155
+
156
+ // Sort directories so priority ones are walked first
157
+ directories.sort((a, b) => {
158
+ const aBase = path.basename(a).toLowerCase();
159
+ const bBase = path.basename(b).toLowerCase();
160
+ const aPriority = priorityDirs.includes(aBase) || priorityDirs.some(p => a.includes(p));
161
+ const bPriority = priorityDirs.includes(bBase) || priorityDirs.some(p => b.includes(p));
162
+ if (aPriority && !bPriority) return -1;
163
+ if (!aPriority && bPriority) return 1;
164
+ return 0;
165
+ });
166
+
167
+ // Walk directories
168
+ for (const dir of directories) {
169
+ await walk(dir);
170
+ if (collected.length >= MAX_FILES || totalSize >= MAX_SIZE) return;
171
+ }
172
+
173
+ // Process files in this directory
174
+ for (const file of files) {
175
+ const ext = path.extname(file.name).toLowerCase();
176
+ if (!allowedExtensions.has(ext)) continue;
177
+
178
+ const relativePath = path.relative(directory, file.fullPath);
179
+ if (collectedPaths.has(relativePath)) continue;
180
+
181
+ try {
182
+ const stats = await fs.stat(file.fullPath);
183
+ // Skip files that are individually too large to allow variety
184
+ if (stats.size > 50 * 1024) continue;
185
+
186
+ if (totalSize + stats.size > MAX_SIZE) continue;
187
+
188
+ const content = await fs.readFile(file.fullPath, 'utf8');
189
+ collected.push({
190
+ filepath: relativePath,
191
+ content: content
192
+ });
193
+ collectedPaths.add(relativePath);
194
+ totalSize += stats.size;
195
+
196
+ if (collected.length >= MAX_FILES) break;
197
+ } catch (err) {
198
+ // Skip files we can't read
199
+ }
200
+ }
201
+ }
202
+
203
+ await walk(directory);
204
+ return collected;
205
+ }
206
+
207
+ function normalizeTrivyData(scanData) {
208
+ if (!scanData.Results) return scanData;
209
+
210
+ const Results = scanData.Results.map(result => {
211
+ const vulnerabilities = [...(result.Vulnerabilities || [])];
212
+
213
+ // Map Secrets
214
+ if (result.Secrets && result.Secrets.length > 0) {
215
+ result.Secrets.forEach(sec => {
216
+ vulnerabilities.push({
217
+ VulnerabilityID: sec.RuleID || 'SecretLeak',
218
+ PkgName: sec.Category || 'secret',
219
+ InstalledVersion: 'Leaked secret detected',
220
+ FixedVersion: 'Remove the hardcoded credentials and load them from environment variables instead.',
221
+ Title: sec.Title || 'Hardcoded Secret Detected',
222
+ Description: `Secret leak detected: ${sec.Title || 'credential'}. Code match at line ${sec.StartLine}: "${(sec.Match || '').trim()}"`,
223
+ Severity: sec.Severity || 'HIGH',
224
+ PrimaryURL: 'https://aquasecurity.github.io/trivy/latest/docs/scanner/secret/'
225
+ });
226
+ });
227
+ }
228
+
229
+ // Map Misconfigurations
230
+ if (result.Misconfigurations && result.Misconfigurations.length > 0) {
231
+ result.Misconfigurations.forEach(mis => {
232
+ vulnerabilities.push({
233
+ VulnerabilityID: mis.ID || 'Misconfiguration',
234
+ PkgName: mis.Type || 'config',
235
+ InstalledVersion: 'Misconfiguration detected',
236
+ FixedVersion: mis.Resolution || 'Update the configuration to comply with security guidelines.',
237
+ Title: mis.Title || 'Security Misconfiguration',
238
+ Description: `${mis.Message || 'Config violation'}\n\nResolution: ${mis.Resolution || ''}`,
239
+ Severity: mis.Severity || 'HIGH',
240
+ PrimaryURL: mis.PrimaryURL || 'https://aquasecurity.github.io/trivy/latest/docs/scanner/misconfig/'
241
+ });
242
+ });
243
+ }
244
+
245
+ return {
246
+ ...result,
247
+ Vulnerabilities: vulnerabilities
248
+ };
249
+ });
250
+
251
+ return {
252
+ ...scanData,
253
+ Results
254
+ };
255
+ }
256
+
257
+ function filterHighCritical(scanData) {
258
+ if (!scanData.Results) return scanData;
259
+
260
+ return {
261
+ ...scanData,
262
+ Results: scanData.Results.map(result => {
263
+ if (!result.Vulnerabilities) return result;
264
+
265
+ // Filter only HIGH and CRITICAL
266
+ const filteredVulns = result.Vulnerabilities.filter(
267
+ vuln => vuln.Severity === 'HIGH' || vuln.Severity === 'CRITICAL'
268
+ );
269
+
270
+ return {
271
+ ...result,
272
+ Vulnerabilities: filteredVulns
273
+ };
274
+ }).filter(result => result.Vulnerabilities?.length > 0)
275
+ };
276
+ }
277
+
278
+ async function sendToAPI(apiUrl, scanData, directory, token, codeFiles) {
279
+ try {
280
+ const headers = {
281
+ 'Content-Type': 'application/json',
282
+ 'X-Device-ID': getDeviceId()
283
+ };
284
+ if (token) {
285
+ headers['Authorization'] = `Bearer ${token}`;
286
+ }
287
+
288
+ const response = await axios.post(apiUrl, {
289
+ vulnerabilities: scanData.Results || [],
290
+ targetName: path.basename(path.resolve(directory || '.')),
291
+ codeFiles: codeFiles || []
292
+ }, {
293
+ headers
294
+ });
295
+ return response.data;
296
+ } catch (error) {
297
+ throw new Error(error.response?.data?.message || error.message);
298
+ }
299
+ }
300
+
301
+ async function pollJobStatus(rawApiUrl, jobId, token) {
302
+ const jobUrl = rawApiUrl.endsWith('/')
303
+ ? `${rawApiUrl}api/audit/jobs/${jobId}`
304
+ : `${rawApiUrl}/api/audit/jobs/${jobId}`;
305
+
306
+ const headers = {};
307
+ if (token) {
308
+ headers['Authorization'] = `Bearer ${token}`;
309
+ }
310
+
311
+ const stages = [
312
+ 'Initializing audit task...',
313
+ 'Compiling scan chunks...',
314
+ 'Analyzing code architecture...',
315
+ 'Running deep SAST logic checks...',
316
+ 'Detecting access control flaws...',
317
+ 'Verifying inputs & parameters...',
318
+ 'Applying Vibe AI contextual analysis...',
319
+ 'Calculating delta changes...',
320
+ 'Finalizing security reports...'
321
+ ];
322
+ let stageIdx = 0;
323
+
324
+ console.log(chalk.cyan('\n⏳ Monitoring security pipeline in real-time...'));
325
+
326
+ let completed = 0;
327
+ let total = 0;
328
+ let progress = 0;
329
+ let stage = 'Initializing audit task...';
330
+ let spinnerIdx = 0;
331
+ const spinnerFrames = ['/', '-', '\\', '|'];
332
+
333
+ // Start smooth terminal spinner render loop
334
+ const spinnerId = setInterval(() => {
335
+ const frame = spinnerFrames[spinnerIdx % spinnerFrames.length];
336
+ spinnerIdx++;
337
+ process.stdout.write(
338
+ `\r${chalk.cyan(frame)} [${completed}/${total} Chunks - ${progress}%] ${chalk.gray(stage)} `
339
+ );
340
+ }, 120);
341
+
342
+ return new Promise((resolve) => {
343
+ let intervalId = setInterval(async () => {
344
+ try {
345
+ const response = await axios.get(jobUrl, { headers });
346
+ const { job } = response.data;
347
+
348
+ if (job) {
349
+ const { status, total_chunks, completed_chunks } = job;
350
+
351
+ if (status === 'completed') {
352
+ clearInterval(intervalId);
353
+ clearInterval(spinnerId);
354
+ process.stdout.write(`\r${chalk.green('»')} [${completed_chunks}/${total_chunks} Chunks - 100%] Scan complete! \n`);
355
+ console.log(chalk.green.bold(`\n🎉 Scan complete! Check web dashboard for the results.`));
356
+ await promptForDashboard();
357
+ resolve();
358
+ } else if (status === 'failed') {
359
+ clearInterval(intervalId);
360
+ clearInterval(spinnerId);
361
+ process.stdout.write(`\r${chalk.red('»')} [${completed_chunks}/${total_chunks} Chunks] Pipeline failed. \n`);
362
+ console.log(chalk.red.bold(`\n❌ Audit pipeline failed: Worker reports job failed.`));
363
+ resolve();
364
+ } else {
365
+ // Update state for spinner loop to render
366
+ completed = completed_chunks;
367
+ total = total_chunks;
368
+ progress = total_chunks > 0 ? Math.round((completed_chunks / total_chunks) * 100) : 0;
369
+ stage = stages[stageIdx % stages.length];
370
+ stageIdx++;
371
+ }
372
+ }
373
+ } catch (err) {
374
+ stage = 'Connection blip... retrying status check...';
375
+ }
376
+ }, 2000);
377
+ });
378
+ }
379
+
380
+ async function promptForDashboard() {
381
+ const rl = readline.createInterface({
382
+ input: process.stdin,
383
+ output: process.stdout
384
+ });
385
+
386
+ const webUrl = getWebUrl();
387
+
388
+ return new Promise((resolve) => {
389
+ rl.question(chalk.cyan('\n❓ Go to my dashboard? (yes/no): '), (answer) => {
390
+ rl.close();
391
+ const sanitized = answer.trim().toLowerCase();
392
+ if (sanitized === 'yes' || sanitized === 'y') {
393
+ const dashboardUrl = `${webUrl}/dashboard/history`;
394
+ console.log(chalk.gray(`Opening your default browser to: ${dashboardUrl}`));
395
+ const openCmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
396
+ exec(`${openCmd} "${dashboardUrl}"`, (err) => {
397
+ if (err) {
398
+ console.log(chalk.yellow(`Could not open browser automatically. Please open this link manually:\n ${chalk.underline(dashboardUrl)}`));
399
+ }
400
+ });
401
+ }
402
+ resolve();
403
+ });
404
+ });
405
+ }
406
+
@@ -0,0 +1,87 @@
1
+ import os from 'os';
2
+ import path from 'path';
3
+ import fs from 'fs-extra';
4
+ import crypto from 'crypto';
5
+
6
+ const CONFIG_DIR = path.join(os.homedir(), '.vibe');
7
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
8
+
9
+ export function readConfig() {
10
+ try {
11
+ if (fs.existsSync(CONFIG_FILE)) {
12
+ return fs.readJsonSync(CONFIG_FILE);
13
+ }
14
+ } catch (error) {
15
+ // Ignore reading error, return empty object
16
+ }
17
+ return {};
18
+ }
19
+
20
+ export function writeConfig(data) {
21
+ try {
22
+ fs.ensureDirSync(CONFIG_DIR);
23
+ fs.writeJsonSync(CONFIG_FILE, data, { spaces: 2 });
24
+ return true;
25
+ } catch (error) {
26
+ console.error('Failed to write CLI config:', error.message);
27
+ return false;
28
+ }
29
+ }
30
+
31
+ export function getToken() {
32
+ const config = readConfig();
33
+ return config.token || null;
34
+ }
35
+
36
+ export function setToken(token, email) {
37
+ const config = readConfig();
38
+ config.token = token;
39
+ config.email = email;
40
+ return writeConfig(config);
41
+ }
42
+
43
+ export function clearToken() {
44
+ const config = readConfig();
45
+ delete config.token;
46
+ delete config.email;
47
+ return writeConfig(config);
48
+ }
49
+
50
+ export function getApiUrl(optionUrl) {
51
+ if (optionUrl) return optionUrl;
52
+ const config = readConfig();
53
+ return config.apiUrl || 'https://vibe-audit-theta.vercel.app';
54
+ }
55
+
56
+ export function getWebUrl(optionUrl) {
57
+ if (optionUrl) return optionUrl;
58
+ const config = readConfig();
59
+ return config.webUrl || 'https://vibeaduditai.vercel.app';
60
+ }
61
+
62
+ export function getDeviceId() {
63
+ const config = readConfig();
64
+ if (config.deviceId) {
65
+ return config.deviceId;
66
+ }
67
+
68
+ // Build a stable machine-specific fingerprint
69
+ let fingerprint = 'fallback-device-vibe';
70
+ try {
71
+ fingerprint = [
72
+ os.hostname(),
73
+ os.userInfo().username,
74
+ os.platform(),
75
+ os.arch(),
76
+ os.release()
77
+ ].join('-');
78
+ } catch (err) {
79
+ // Fallback to random UUID if os details are not accessible
80
+ fingerprint = crypto.randomUUID();
81
+ }
82
+
83
+ const deviceId = crypto.createHash('sha256').update(fingerprint).digest('hex');
84
+ config.deviceId = deviceId;
85
+ writeConfig(config);
86
+ return deviceId;
87
+ }
@@ -0,0 +1,130 @@
1
+ import axios from 'axios';
2
+ import { extract } from 'tar';
3
+ import fs from 'fs-extra';
4
+ import path from 'path';
5
+ import os from 'os';
6
+ import AdmZip from 'adm-zip';
7
+
8
+ const VIBE_AUDIT_HOME = path.join(os.homedir(), '.vibe-audit-scan');
9
+ const BIN_DIR = path.join(VIBE_AUDIT_HOME, 'bin');
10
+ const TRIVY_BINARY_PATH = path.join(BIN_DIR, os.platform() === 'win32' ? 'trivy.exe' : 'trivy');
11
+
12
+ export async function ensureTrivyInstalled() {
13
+ // First check if Trivy is already in PATH
14
+ if (await isSystemTrivyAvailable()) {
15
+ return 'trivy';
16
+ }
17
+
18
+ // Then check if we have a local installation
19
+ if (await isLocalTrivyAvailable()) {
20
+ return TRIVY_BINARY_PATH;
21
+ }
22
+
23
+ // If neither, install it locally
24
+ console.log('⬇️ Core scan engine not found, downloading components...');
25
+ await installLocalTrivy();
26
+ return TRIVY_BINARY_PATH;
27
+ }
28
+
29
+ async function isSystemTrivyAvailable() {
30
+ try {
31
+ const { exec } = await import('child_process');
32
+ const { promisify } = await import('util');
33
+ const execPromise = promisify(exec);
34
+ await execPromise('trivy --version');
35
+ return true;
36
+ } catch (error) {
37
+ return false;
38
+ }
39
+ }
40
+
41
+ async function isLocalTrivyAvailable() {
42
+ try {
43
+ return await fs.pathExists(TRIVY_BINARY_PATH);
44
+ } catch (error) {
45
+ return false;
46
+ }
47
+ }
48
+
49
+ async function installLocalTrivy() {
50
+ // Create directories
51
+ await fs.ensureDir(VIBE_AUDIT_HOME);
52
+ await fs.ensureDir(BIN_DIR);
53
+
54
+ // Get OS and arch
55
+ const platform = os.platform();
56
+ const arch = os.arch();
57
+
58
+ // Map to Trivy's naming convention
59
+ const osMap = {
60
+ 'darwin': 'macOS',
61
+ 'linux': 'Linux',
62
+ 'win32': 'Windows'
63
+ };
64
+ const archMap = {
65
+ 'x64': '64bit',
66
+ 'arm64': 'ARM64'
67
+ };
68
+
69
+ const trivyOs = osMap[platform];
70
+ const trivyArch = archMap[arch];
71
+
72
+ if (!trivyOs || !trivyArch) {
73
+ throw new Error(`Unsupported platform/arch: ${platform}/${arch}`);
74
+ }
75
+
76
+ // Get latest release from GitHub
77
+ console.log('🔍 Fetching scan engine updates...');
78
+ const releaseRes = await axios.get('https://api.github.com/repos/aquasecurity/trivy/releases/latest');
79
+ const latestVersion = releaseRes.data.tag_name.replace('v', '');
80
+
81
+ // Construct download URL
82
+ const ext = platform === 'win32' ? 'zip' : 'tar.gz';
83
+ const assetName = `trivy_${latestVersion}_${trivyOs}-${trivyArch}.${ext}`;
84
+ const asset = releaseRes.data.assets.find(a => a.name === assetName);
85
+
86
+ if (!asset) {
87
+ throw new Error(`Could not find Trivy asset: ${assetName}`);
88
+ }
89
+
90
+ // Download the asset
91
+ console.log(`📥 Downloading scanner updates v${latestVersion}...`);
92
+ const downloadRes = await axios.get(asset.browser_download_url, {
93
+ responseType: 'arraybuffer'
94
+ });
95
+
96
+ // Create temp file
97
+ const tempPath = path.join(os.tmpdir(), `trivy_${latestVersion}.${ext}`);
98
+ await fs.writeFile(tempPath, downloadRes.data);
99
+
100
+ // Extract the binary
101
+ console.log('📦 Extracting scanner components...');
102
+ if (platform === 'win32') {
103
+ // Extract zip for Windows
104
+ const zip = new AdmZip(tempPath);
105
+ const zipEntries = zip.getEntries();
106
+ for (const entry of zipEntries) {
107
+ if (entry.name.endsWith('trivy.exe')) {
108
+ zip.extractEntryTo(entry, BIN_DIR, false, true);
109
+ break;
110
+ }
111
+ }
112
+ } else {
113
+ // Extract tar.gz for macOS/Linux
114
+ await extract({
115
+ file: tempPath,
116
+ cwd: BIN_DIR,
117
+ filter: (path) => path === 'trivy'
118
+ });
119
+ }
120
+
121
+ // Make executable (only for non-Windows)
122
+ if (os.platform() !== 'win32') {
123
+ await fs.chmod(TRIVY_BINARY_PATH, '755');
124
+ }
125
+
126
+ // Clean up temp file
127
+ await fs.remove(tempPath);
128
+
129
+ console.log('✅ Core scan engine initialized successfully!');
130
+ }
@@ -0,0 +1,71 @@
1
+ import chalk from 'chalk';
2
+ import boxen from 'boxen';
3
+
4
+ const asciiArt = [
5
+ '█████ █████ █████ ███████████ ██████████ █████████ █████ █████ ██████████ █████ ███████████ ',
6
+ '░░███ ░░███ ░░███ ░░███░░░░░███░░███░░░░░█ ███░░░░░███ ░░███ ░░███ ░░███░░░░███ ░░███ ░█░░░███░░░█ ',
7
+ ' ░███ ░███ ░███ ░███ ░███ ░███ █ ░ ░███ ░███ ░███ ░███ ░███ ░░███ ░███ ░ ░███ ░ ',
8
+ ' ░███ ░███ ░███ ░██████████ ░██████ ░███████████ ░███ ░███ ░███ ░███ ░███ ░███ ',
9
+ ' ░░███ ███ ░███ ░███░░░░░███ ░███░░█ ░███░░░░░███ ░███ ░███ ░███ ░███ ░███ ░███ ',
10
+ ' ░░░█████░ ░███ ░███ ░███ ░███ ░ █ ░███ ░███ ░███ ░███ ░███ ███ ░███ ░███ ',
11
+ ' ░░███ █████ ███████████ ██████████ █████ █████ ░░████████ ██████████ █████ █████ ',
12
+ ' ░░░ ░░░░░ ░░░░░░░░░░░ ░░░░░░░░░░ ░░░░░ ░░░░░ ░░░░░░░░ ░░░░░░░░░░ ░░░░░ ░░░░░ '
13
+ ];
14
+
15
+ export function printWelcome() {
16
+ // Print ASCII art first
17
+ console.log('');
18
+ asciiArt.forEach(line => {
19
+ console.log(chalk.cyan(line));
20
+ });
21
+ console.log('');
22
+
23
+ const welcomeContent = `
24
+ ${chalk.cyan.bold('Vibe Audit')}${chalk.gray(' v1.2.6 | Terminal-First Security')}
25
+ ${chalk.gray('-'.repeat(50))}
26
+
27
+ ${chalk.white.bold('🚀 Ready to secure your code?')}
28
+
29
+ ${chalk.cyan.bold('Quick Start:')}
30
+ ${chalk.gray('$')} ${chalk.white('vibe scan .')}${chalk.gray(' # Scan current directory')}
31
+ ${chalk.gray('$')} ${chalk.white('vibe login')}${chalk.gray(' # Authenticate your account')}
32
+
33
+ ${chalk.cyan.bold('Need help?')}
34
+ ${chalk.gray('$')} ${chalk.white('vibe --help')}${chalk.gray(' # View all available commands')}
35
+ ${chalk.gray('$')} ${chalk.white('vibe cheatsheet')}${chalk.gray(' # List shortcuts and status')}
36
+ `;
37
+
38
+ console.log(boxen(welcomeContent, {
39
+ padding: 1,
40
+ margin: 1,
41
+ borderStyle: 'round',
42
+ borderColor: 'cyan',
43
+ titleAlignment: 'left'
44
+ }));
45
+ }
46
+
47
+ export function printHelpCheatsheet() {
48
+ const cheatsheet = `
49
+ ${chalk.cyan.bold('Vibe Audit Command Cheatsheet')}
50
+
51
+ ${chalk.white.bold('Auth Commands:')}
52
+ ${chalk.white('vibe login')}${chalk.gray(' - Authenticate your CLI with the Vibe Cloud backend')}
53
+ ${chalk.white('vibe whoami')}${chalk.gray(' - Displays the currently logged-in user profile')}
54
+ ${chalk.white('vibe logout')}${chalk.gray(' - Revokes your session and clears local tokens')}
55
+
56
+ ${chalk.white.bold('Scan Commands:')}
57
+ ${chalk.white('vibe scan <dir>')}${chalk.gray(' - Scans a directory and sends results to the AI engine')}
58
+ ${chalk.white('vibe status <id>')}${chalk.gray(' - Checks the real-time processing status of an audit')}
59
+
60
+ ${chalk.white.bold('System Commands:')}
61
+ ${chalk.white('vibe cheatsheet')}${chalk.gray(' - Lists all commands and current system status')}
62
+ ${chalk.white('vibe install-deps')}${chalk.gray(' - Auto-installs/verifies Trivy binary dependencies')}
63
+ `;
64
+
65
+ console.log(boxen(cheatsheet, {
66
+ padding: 1,
67
+ margin: 1,
68
+ borderStyle: 'round',
69
+ borderColor: 'cyan'
70
+ }));
71
+ }