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 +87 -0
- package/bin/vibe-audit.js +109 -0
- package/package.json +24 -0
- package/src/commands/auth.js +226 -0
- package/src/commands/scan.js +406 -0
- package/src/utils/config.js +87 -0
- package/src/utils/trivyInstaller.js +130 -0
- package/src/utils/welcome.js +71 -0
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, '&')
|
|
53
|
+
.replace(/</g, '<')
|
|
54
|
+
.replace(/>/g, '>')
|
|
55
|
+
.replace(/"/g, '"')
|
|
56
|
+
.replace(/'/g, ''');
|
|
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
|
+
}
|