grepleaks 1.3.0 → 1.3.2
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/bin/grepleaks.js +327 -665
- package/package.json +1 -1
- package/README.md +0 -94
package/bin/grepleaks.js
CHANGED
|
@@ -1,797 +1,459 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
-
const
|
|
5
|
+
const { execSync } = require('child_process');
|
|
6
6
|
const https = require('https');
|
|
7
7
|
const http = require('http');
|
|
8
|
-
const readline = require('readline');
|
|
9
|
-
const { exec } = require('child_process');
|
|
10
8
|
const archiver = require('archiver');
|
|
9
|
+
const readline = require('readline');
|
|
11
10
|
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
const WEB_URL = 'https://grepleaks.com';
|
|
15
|
-
const CONFIG_DIR = path.join(os.homedir(), '.grepleaks');
|
|
16
|
-
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
17
|
-
const CREDENTIALS_FILE = path.join(CONFIG_DIR, 'credentials.json');
|
|
18
|
-
|
|
19
|
-
// Text strings (English only)
|
|
20
|
-
const TEXT = {
|
|
21
|
-
// General
|
|
22
|
-
tagline: 'Security scanner for your code',
|
|
23
|
-
version: 'grepleaks v',
|
|
24
|
-
|
|
25
|
-
// Help
|
|
26
|
-
helpUsage: 'USAGE:',
|
|
27
|
-
helpCommands: 'COMMANDS:',
|
|
28
|
-
helpOptions: 'OPTIONS:',
|
|
29
|
-
helpExamples: 'EXAMPLES:',
|
|
30
|
-
helpMoreInfo: 'MORE INFO:',
|
|
31
|
-
helpLoginDesc: 'Login (email or GitHub)',
|
|
32
|
-
helpLogoutDesc: 'Logout',
|
|
33
|
-
helpScanDesc: 'Scan a directory',
|
|
34
|
-
helpShowHelp: 'Show help',
|
|
35
|
-
helpShowVersion: 'Show version',
|
|
36
|
-
|
|
37
|
-
// Login
|
|
38
|
-
loginTitle: 'Login to grepleaks',
|
|
39
|
-
loginChooseMethod: 'Choose login method:',
|
|
40
|
-
loginOptionEmail: 'Email / Password',
|
|
41
|
-
loginOptionGitHub: 'GitHub',
|
|
42
|
-
loginChoice: 'Your choice (1 or 2): ',
|
|
43
|
-
loginGitHubTitle: 'Login with GitHub',
|
|
44
|
-
loginOpeningBrowser: 'Opening browser...',
|
|
45
|
-
loginTokenPrompt: 'After logging in on the website, copy your token and paste it here.',
|
|
46
|
-
loginTokenInput: 'Token: ',
|
|
47
|
-
loginTokenRequired: 'Token required',
|
|
48
|
-
loginVerifying: 'Verifying token...',
|
|
49
|
-
loginInvalidToken: 'Invalid token',
|
|
50
|
-
loginEmail: 'Email: ',
|
|
51
|
-
loginPassword: 'Password: ',
|
|
52
|
-
loginEmailPasswordRequired: 'Email and password required',
|
|
53
|
-
loginInProgress: 'Logging in...',
|
|
54
|
-
loginFailed: 'Login failed',
|
|
55
|
-
loginSuccess: 'Logged in as',
|
|
56
|
-
|
|
57
|
-
// Logout
|
|
58
|
-
logoutSuccess: 'Logged out',
|
|
59
|
-
|
|
60
|
-
// Scan
|
|
61
|
-
scanTitle: 'Security scan',
|
|
62
|
-
scanMustLogin: 'You must be logged in to scan.',
|
|
63
|
-
scanPathNotFound: 'Path not found:',
|
|
64
|
-
scanMustBeDir: 'Path must be a directory:',
|
|
65
|
-
scanProject: 'Project:',
|
|
66
|
-
scanPath: 'Path:',
|
|
67
|
-
scanPreparing: 'Preparing files...',
|
|
68
|
-
scanArchiveCreated: 'Archive created',
|
|
69
|
-
scanUploading: 'Uploading to server...',
|
|
70
|
-
scanUploadFailed: 'Upload failed',
|
|
71
|
-
scanStarted: 'Scan started',
|
|
72
|
-
scanAnalyzing: 'Analyzing...',
|
|
73
|
-
scanStatusFailed: 'Failed to check status',
|
|
74
|
-
scanFailed: 'Scan failed',
|
|
75
|
-
scanComplete: 'Scan complete!',
|
|
76
|
-
scanResults: 'Results:',
|
|
77
|
-
scanScore: 'Score:',
|
|
78
|
-
scanVulnerabilities: 'Vulnerabilities:',
|
|
79
|
-
scanReportSaved: 'Report saved:',
|
|
80
|
-
|
|
81
|
-
// Report
|
|
82
|
-
reportTitle: 'Security Report',
|
|
83
|
-
reportNoVulns: 'No vulnerabilities detected',
|
|
84
|
-
reportCongrats: 'Congratulations! Your code has no known vulnerabilities.',
|
|
85
|
-
reportVulnsDetected: 'Vulnerabilities detected',
|
|
86
|
-
reportRecommendation: 'Recommendation',
|
|
87
|
-
reportGeneratedBy: 'Report generated by',
|
|
88
|
-
|
|
89
|
-
// Errors
|
|
90
|
-
errorUnknownCmd: 'Unknown command:',
|
|
91
|
-
errorUseHelp: "Use 'grepleaks --help' for help."
|
|
92
|
-
};
|
|
11
|
+
const API_URL = 'http://72.62.60.91';
|
|
12
|
+
const CONFIG_FILE = '.grepleaksrc';
|
|
93
13
|
|
|
94
|
-
//
|
|
95
|
-
const
|
|
96
|
-
'node_modules', '.git', '.svn', '.hg', 'vendor', '__pycache__',
|
|
97
|
-
'.venv', 'venv', 'env', '.env', 'dist', 'build', '.next',
|
|
98
|
-
'.nuxt', 'coverage', '.nyc_output', '.cache', '.parcel-cache',
|
|
99
|
-
'target', 'Pods', '.gradle', '.idea', '.vscode', '*.log',
|
|
100
|
-
'*.lock', 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml',
|
|
101
|
-
'*.min.js', '*.min.css', '*.map', '*.png', '*.jpg', '*.jpeg',
|
|
102
|
-
'*.gif', '*.ico', '*.svg', '*.woff', '*.woff2', '*.ttf', '*.eot',
|
|
103
|
-
'*.mp3', '*.mp4', '*.avi', '*.mov', '*.pdf', '*.zip', '*.tar',
|
|
104
|
-
'*.gz', '*.rar', '*.7z', '*.exe', '*.dll', '*.so', '*.dylib'
|
|
105
|
-
];
|
|
106
|
-
|
|
107
|
-
// Colors
|
|
108
|
-
const c = {
|
|
14
|
+
// Colors for terminal output
|
|
15
|
+
const colors = {
|
|
109
16
|
reset: '\x1b[0m',
|
|
110
|
-
bold: '\x1b[1m',
|
|
111
17
|
red: '\x1b[31m',
|
|
112
18
|
green: '\x1b[32m',
|
|
113
19
|
yellow: '\x1b[33m',
|
|
114
20
|
blue: '\x1b[34m',
|
|
115
21
|
cyan: '\x1b[36m',
|
|
116
|
-
|
|
22
|
+
bold: '\x1b[1m',
|
|
117
23
|
};
|
|
118
24
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
return TEXT[key] || key;
|
|
25
|
+
function log(message, color = '') {
|
|
26
|
+
console.log(`${color}${message}${colors.reset}`);
|
|
122
27
|
}
|
|
123
28
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
console.log(`${color}${msg}${c.reset}`);
|
|
29
|
+
function logError(message) {
|
|
30
|
+
log(`Error: ${message}`, colors.red);
|
|
127
31
|
}
|
|
128
32
|
|
|
129
|
-
function
|
|
130
|
-
|
|
131
|
-
process.exit(1);
|
|
33
|
+
function logSuccess(message) {
|
|
34
|
+
log(`${message}`, colors.green);
|
|
132
35
|
}
|
|
133
36
|
|
|
134
|
-
function
|
|
135
|
-
log(`${
|
|
37
|
+
function logInfo(message) {
|
|
38
|
+
log(`${message}`, colors.cyan);
|
|
136
39
|
}
|
|
137
40
|
|
|
138
|
-
|
|
139
|
-
|
|
41
|
+
// Get config
|
|
42
|
+
function getConfig() {
|
|
43
|
+
const configPath = path.join(process.cwd(), CONFIG_FILE);
|
|
44
|
+
if (fs.existsSync(configPath)) {
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
47
|
+
} catch (e) {
|
|
48
|
+
return {};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return {};
|
|
140
52
|
}
|
|
141
53
|
|
|
142
|
-
//
|
|
143
|
-
function
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
54
|
+
// Save config
|
|
55
|
+
function saveConfig(config) {
|
|
56
|
+
const configPath = path.join(process.cwd(), CONFIG_FILE);
|
|
57
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
147
58
|
}
|
|
148
59
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
return { lang: 'en' };
|
|
152
|
-
}
|
|
60
|
+
// Check if inside a git repo
|
|
61
|
+
function isGitRepo() {
|
|
153
62
|
try {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
63
|
+
execSync('git rev-parse --git-dir', { stdio: 'ignore' });
|
|
64
|
+
return true;
|
|
65
|
+
} catch (e) {
|
|
66
|
+
return false;
|
|
157
67
|
}
|
|
158
68
|
}
|
|
159
69
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
const current = loadConfig();
|
|
163
|
-
fs.writeFileSync(CONFIG_FILE, JSON.stringify({ ...current, ...data }, null, 2));
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// Credentials management
|
|
167
|
-
function saveCredentials(data) {
|
|
168
|
-
ensureConfigDir();
|
|
169
|
-
fs.writeFileSync(CREDENTIALS_FILE, JSON.stringify(data, null, 2));
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function loadCredentials() {
|
|
173
|
-
if (!fs.existsSync(CREDENTIALS_FILE)) {
|
|
174
|
-
return null;
|
|
175
|
-
}
|
|
70
|
+
// Get git hooks directory
|
|
71
|
+
function getGitHooksDir() {
|
|
176
72
|
try {
|
|
177
|
-
|
|
178
|
-
|
|
73
|
+
const gitDir = execSync('git rev-parse --git-dir', { encoding: 'utf8' }).trim();
|
|
74
|
+
return path.join(gitDir, 'hooks');
|
|
75
|
+
} catch (e) {
|
|
179
76
|
return null;
|
|
180
77
|
}
|
|
181
78
|
}
|
|
182
79
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
80
|
+
// Create git hook
|
|
81
|
+
function createHook(hookName, apiKey) {
|
|
82
|
+
const hooksDir = getGitHooksDir();
|
|
83
|
+
if (!hooksDir) {
|
|
84
|
+
logError('Not a git repository');
|
|
85
|
+
return false;
|
|
186
86
|
}
|
|
187
|
-
}
|
|
188
87
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
input: process.stdin,
|
|
193
|
-
output: process.stdout
|
|
194
|
-
});
|
|
195
|
-
return new Promise(resolve => {
|
|
196
|
-
rl.question(question, answer => {
|
|
197
|
-
rl.close();
|
|
198
|
-
resolve(answer);
|
|
199
|
-
});
|
|
200
|
-
});
|
|
201
|
-
}
|
|
88
|
+
if (!fs.existsSync(hooksDir)) {
|
|
89
|
+
fs.mkdirSync(hooksDir, { recursive: true });
|
|
90
|
+
}
|
|
202
91
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
});
|
|
92
|
+
const hookPath = path.join(hooksDir, hookName);
|
|
93
|
+
const hookContent = `#!/bin/sh
|
|
94
|
+
# Grepleaks security scan hook
|
|
95
|
+
npx grepleaks scan --api-key "${apiKey}"
|
|
96
|
+
`;
|
|
209
97
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
if (stdin.setRawMode) stdin.setRawMode(true);
|
|
215
|
-
|
|
216
|
-
let password = '';
|
|
217
|
-
|
|
218
|
-
const onData = (char) => {
|
|
219
|
-
char = char.toString();
|
|
220
|
-
|
|
221
|
-
switch (char) {
|
|
222
|
-
case '\n':
|
|
223
|
-
case '\r':
|
|
224
|
-
case '\u0004':
|
|
225
|
-
if (stdin.setRawMode) stdin.setRawMode(wasRaw);
|
|
226
|
-
stdin.removeListener('data', onData);
|
|
227
|
-
rl.close();
|
|
228
|
-
console.log();
|
|
229
|
-
resolve(password);
|
|
230
|
-
break;
|
|
231
|
-
case '\u0003':
|
|
232
|
-
process.exit();
|
|
233
|
-
break;
|
|
234
|
-
case '\u007F':
|
|
235
|
-
password = password.slice(0, -1);
|
|
236
|
-
break;
|
|
237
|
-
default:
|
|
238
|
-
password += char;
|
|
239
|
-
break;
|
|
240
|
-
}
|
|
241
|
-
};
|
|
98
|
+
fs.writeFileSync(hookPath, hookContent);
|
|
99
|
+
fs.chmodSync(hookPath, '755');
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
242
102
|
|
|
243
|
-
|
|
244
|
-
|
|
103
|
+
// Remove git hook
|
|
104
|
+
function removeHook(hookName) {
|
|
105
|
+
const hooksDir = getGitHooksDir();
|
|
106
|
+
if (!hooksDir) return;
|
|
107
|
+
|
|
108
|
+
const hookPath = path.join(hooksDir, hookName);
|
|
109
|
+
if (fs.existsSync(hookPath)) {
|
|
110
|
+
const content = fs.readFileSync(hookPath, 'utf8');
|
|
111
|
+
if (content.includes('Grepleaks')) {
|
|
112
|
+
fs.unlinkSync(hookPath);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
245
115
|
}
|
|
246
116
|
|
|
247
|
-
//
|
|
248
|
-
function
|
|
117
|
+
// Create zip of current directory
|
|
118
|
+
async function createZip() {
|
|
249
119
|
return new Promise((resolve, reject) => {
|
|
250
|
-
const
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
port: urlObj.port || (urlObj.protocol === 'https:' ? 443 : 80),
|
|
254
|
-
path: urlObj.pathname + urlObj.search,
|
|
255
|
-
method,
|
|
256
|
-
headers: {
|
|
257
|
-
'Content-Type': 'application/json',
|
|
258
|
-
...headers
|
|
259
|
-
}
|
|
260
|
-
};
|
|
120
|
+
const zipPath = path.join(process.cwd(), '.grepleaks-scan.zip');
|
|
121
|
+
const output = fs.createWriteStream(zipPath);
|
|
122
|
+
const archive = archiver('zip', { zlib: { level: 9 } });
|
|
261
123
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
let body = '';
|
|
265
|
-
res.on('data', chunk => body += chunk);
|
|
266
|
-
res.on('end', () => {
|
|
267
|
-
try {
|
|
268
|
-
resolve({ status: res.statusCode, data: JSON.parse(body) });
|
|
269
|
-
} catch {
|
|
270
|
-
resolve({ status: res.statusCode, data: body });
|
|
271
|
-
}
|
|
272
|
-
});
|
|
273
|
-
});
|
|
124
|
+
output.on('close', () => resolve(zipPath));
|
|
125
|
+
archive.on('error', (err) => reject(err));
|
|
274
126
|
|
|
275
|
-
|
|
127
|
+
archive.pipe(output);
|
|
276
128
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
129
|
+
// Add all files except .git and node_modules
|
|
130
|
+
archive.glob('**/*', {
|
|
131
|
+
ignore: ['.git/**', 'node_modules/**', '.grepleaks-scan.zip'],
|
|
132
|
+
dot: true,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
archive.finalize();
|
|
281
136
|
});
|
|
282
137
|
}
|
|
283
138
|
|
|
284
|
-
//
|
|
285
|
-
function
|
|
139
|
+
// Send scan request to API
|
|
140
|
+
async function sendScan(zipPath, apiKey) {
|
|
286
141
|
return new Promise((resolve, reject) => {
|
|
287
|
-
const boundary = '----
|
|
288
|
-
const
|
|
289
|
-
const fileStream = fs.createReadStream(filePath);
|
|
142
|
+
const boundary = '----FormBoundary' + Math.random().toString(36).slice(2);
|
|
143
|
+
const fileContent = fs.readFileSync(zipPath);
|
|
290
144
|
|
|
291
|
-
const
|
|
145
|
+
const bodyParts = [
|
|
146
|
+
`--${boundary}\r\n`,
|
|
147
|
+
`Content-Disposition: form-data; name="file"; filename="code.zip"\r\n`,
|
|
148
|
+
`Content-Type: application/zip\r\n\r\n`,
|
|
149
|
+
];
|
|
292
150
|
|
|
151
|
+
const bodyEnd = `\r\n--${boundary}--\r\n`;
|
|
152
|
+
const bodyStart = Buffer.from(bodyParts.join(''));
|
|
153
|
+
const bodyEndBuf = Buffer.from(bodyEnd);
|
|
154
|
+
const body = Buffer.concat([bodyStart, fileContent, bodyEndBuf]);
|
|
155
|
+
|
|
156
|
+
const url = new URL(`${API_URL}/api/v1/scan`);
|
|
293
157
|
const options = {
|
|
294
|
-
hostname:
|
|
295
|
-
port:
|
|
296
|
-
path:
|
|
158
|
+
hostname: url.hostname,
|
|
159
|
+
port: url.port || 80,
|
|
160
|
+
path: url.pathname,
|
|
297
161
|
method: 'POST',
|
|
298
162
|
headers: {
|
|
163
|
+
'X-API-Key': apiKey,
|
|
299
164
|
'Content-Type': `multipart/form-data; boundary=${boundary}`,
|
|
300
|
-
'
|
|
301
|
-
}
|
|
165
|
+
'Content-Length': body.length,
|
|
166
|
+
},
|
|
302
167
|
};
|
|
303
168
|
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
res.on('data', chunk => body += chunk);
|
|
169
|
+
const req = http.request(options, (res) => {
|
|
170
|
+
let data = '';
|
|
171
|
+
res.on('data', (chunk) => data += chunk);
|
|
308
172
|
res.on('end', () => {
|
|
309
173
|
try {
|
|
310
|
-
resolve(
|
|
311
|
-
} catch {
|
|
312
|
-
|
|
174
|
+
resolve(JSON.parse(data));
|
|
175
|
+
} catch (e) {
|
|
176
|
+
reject(new Error(`Invalid response: ${data}`));
|
|
313
177
|
}
|
|
314
178
|
});
|
|
315
179
|
});
|
|
316
180
|
|
|
317
|
-
req.on('error', reject);
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
req.write(`--${boundary}\r\n`);
|
|
321
|
-
req.write(`Content-Disposition: form-data; name="project_name"\r\n\r\n`);
|
|
322
|
-
req.write(projectName || 'Unknown');
|
|
323
|
-
req.write('\r\n');
|
|
324
|
-
|
|
325
|
-
// Add source field
|
|
326
|
-
req.write(`--${boundary}\r\n`);
|
|
327
|
-
req.write(`Content-Disposition: form-data; name="source"\r\n\r\n`);
|
|
328
|
-
req.write('cli');
|
|
329
|
-
req.write('\r\n');
|
|
330
|
-
|
|
331
|
-
// Add file
|
|
332
|
-
req.write(`--${boundary}\r\n`);
|
|
333
|
-
req.write(`Content-Disposition: form-data; name="file"; filename="${fileName}"\r\n`);
|
|
334
|
-
req.write('Content-Type: application/zip\r\n\r\n');
|
|
335
|
-
|
|
336
|
-
fileStream.on('data', chunk => req.write(chunk));
|
|
337
|
-
fileStream.on('end', () => {
|
|
338
|
-
req.write(`\r\n--${boundary}--\r\n`);
|
|
339
|
-
req.end();
|
|
340
|
-
});
|
|
341
|
-
fileStream.on('error', reject);
|
|
342
|
-
});
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// Open URL in browser
|
|
346
|
-
function openBrowser(url) {
|
|
347
|
-
const cmd = process.platform === 'darwin' ? 'open' :
|
|
348
|
-
process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
349
|
-
exec(`${cmd} "${url}"`);
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
// Should skip file/folder
|
|
353
|
-
function shouldSkip(name) {
|
|
354
|
-
return SKIP_PATTERNS.some(pattern => {
|
|
355
|
-
if (pattern.startsWith('*.')) {
|
|
356
|
-
return name.endsWith(pattern.slice(1));
|
|
357
|
-
}
|
|
358
|
-
return name === pattern;
|
|
359
|
-
});
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
// Zip directory
|
|
363
|
-
function zipDirectory(sourceDir, outPath) {
|
|
364
|
-
return new Promise((resolve, reject) => {
|
|
365
|
-
const output = fs.createWriteStream(outPath);
|
|
366
|
-
const archive = archiver('zip', { zlib: { level: 6 } });
|
|
367
|
-
|
|
368
|
-
output.on('close', () => resolve(archive.pointer()));
|
|
369
|
-
archive.on('error', reject);
|
|
370
|
-
|
|
371
|
-
archive.pipe(output);
|
|
372
|
-
|
|
373
|
-
const addDir = (dir, prefix = '') => {
|
|
374
|
-
const items = fs.readdirSync(dir);
|
|
375
|
-
for (const item of items) {
|
|
376
|
-
if (shouldSkip(item)) continue;
|
|
377
|
-
|
|
378
|
-
const fullPath = path.join(dir, item);
|
|
379
|
-
const archivePath = prefix ? `${prefix}/${item}` : item;
|
|
380
|
-
|
|
381
|
-
const stat = fs.statSync(fullPath);
|
|
382
|
-
if (stat.isDirectory()) {
|
|
383
|
-
addDir(fullPath, archivePath);
|
|
384
|
-
} else if (stat.size < 1024 * 1024) {
|
|
385
|
-
archive.file(fullPath, { name: archivePath });
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
};
|
|
389
|
-
|
|
390
|
-
addDir(sourceDir);
|
|
391
|
-
archive.finalize();
|
|
181
|
+
req.on('error', (e) => reject(e));
|
|
182
|
+
req.write(body);
|
|
183
|
+
req.end();
|
|
392
184
|
});
|
|
393
185
|
}
|
|
394
186
|
|
|
395
187
|
// Generate markdown report
|
|
396
|
-
function
|
|
397
|
-
const
|
|
398
|
-
const
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
if (counts.high > 0) md += `| 🟠 High | ${counts.high} |\n`;
|
|
429
|
-
if (counts.medium > 0) md += `| 🟡 Medium | ${counts.medium} |\n`;
|
|
430
|
-
if (counts.low > 0) md += `| 🟢 Low | ${counts.low} |\n`;
|
|
431
|
-
md += `\n`;
|
|
188
|
+
function generateReport(result) {
|
|
189
|
+
const date = new Date().toISOString().split('T')[0];
|
|
190
|
+
const time = new Date().toLocaleTimeString();
|
|
191
|
+
|
|
192
|
+
let report = `# Security Report
|
|
193
|
+
|
|
194
|
+
**Date:** ${date} ${time}
|
|
195
|
+
**Score:** ${result.security_score || 0}/100
|
|
196
|
+
**Grade:** ${result.grade || 'N/A'}
|
|
197
|
+
**Total Findings:** ${result.total_findings || 0}
|
|
198
|
+
|
|
199
|
+
## Summary
|
|
200
|
+
|
|
201
|
+
| Severity | Count |
|
|
202
|
+
|----------|-------|
|
|
203
|
+
| Critical | ${result.critical_count || 0} |
|
|
204
|
+
| High | ${result.high_count || 0} |
|
|
205
|
+
| Medium | ${result.medium_count || 0} |
|
|
206
|
+
| Low | ${result.low_count || 0} |
|
|
207
|
+
|
|
208
|
+
`;
|
|
209
|
+
|
|
210
|
+
if (result.findings && result.findings.length > 0) {
|
|
211
|
+
report += `## Findings\n\n`;
|
|
212
|
+
result.findings.forEach((finding, i) => {
|
|
213
|
+
report += `### ${i + 1}. ${finding.title || finding.rule_id || 'Finding'}\n\n`;
|
|
214
|
+
report += `- **Severity:** ${finding.severity || 'Unknown'}\n`;
|
|
215
|
+
report += `- **File:** ${finding.file || 'N/A'}\n`;
|
|
216
|
+
if (finding.line) report += `- **Line:** ${finding.line}\n`;
|
|
217
|
+
if (finding.description) report += `- **Description:** ${finding.description}\n`;
|
|
218
|
+
report += '\n';
|
|
219
|
+
});
|
|
432
220
|
}
|
|
433
221
|
|
|
434
|
-
|
|
435
|
-
if (vulns.length === 0) {
|
|
436
|
-
md += `## ✅ ${t('reportNoVulns')}\n\n`;
|
|
437
|
-
md += `${t('reportCongrats')}\n`;
|
|
438
|
-
} else {
|
|
439
|
-
md += `---\n\n`;
|
|
440
|
-
md += `## ${t('reportVulnsDetected')}\n\n`;
|
|
441
|
-
|
|
442
|
-
const bySeverity = { CRITICAL: [], HIGH: [], MEDIUM: [], LOW: [] };
|
|
443
|
-
for (const v of vulns) {
|
|
444
|
-
const sev = (v.severity || 'LOW').toUpperCase();
|
|
445
|
-
if (bySeverity[sev]) bySeverity[sev].push(v);
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
for (const [severity, items] of Object.entries(bySeverity)) {
|
|
449
|
-
if (items.length === 0) continue;
|
|
222
|
+
report += `---\n*Generated by [Grepleaks](${API_URL})*\n`;
|
|
450
223
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
severity === 'MEDIUM' ? '🟡' : '🟢';
|
|
454
|
-
|
|
455
|
-
const severityLabel = severity.charAt(0) + severity.slice(1).toLowerCase();
|
|
456
|
-
|
|
457
|
-
md += `### ${emoji} ${severityLabel}\n\n`;
|
|
458
|
-
|
|
459
|
-
for (let i = 0; i < items.length; i++) {
|
|
460
|
-
const v = items[i];
|
|
461
|
-
|
|
462
|
-
// Title from description (first sentence or truncated)
|
|
463
|
-
let title = v.description || 'Security Issue';
|
|
464
|
-
const firstSentence = title.match(/^[^.!?]+[.!?]/);
|
|
465
|
-
if (firstSentence) {
|
|
466
|
-
title = firstSentence[0];
|
|
467
|
-
} else if (title.length > 80) {
|
|
468
|
-
title = title.substring(0, 77) + '...';
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
md += `#### ${i + 1}. ${title}\n\n`;
|
|
472
|
-
|
|
473
|
-
// Location
|
|
474
|
-
if (v.location) {
|
|
475
|
-
md += `- **File:** \`${v.location}\`\n`;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
// CVE
|
|
479
|
-
if (v.cve) {
|
|
480
|
-
md += `- **CVE:** [${v.cve}](https://nvd.nist.gov/vuln/detail/${v.cve})\n`;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
// Package info
|
|
484
|
-
if (v.package_name) {
|
|
485
|
-
md += `- **Package:** \`${v.package_name}\``;
|
|
486
|
-
if (v.current_version) md += ` v${v.current_version}`;
|
|
487
|
-
md += `\n`;
|
|
488
|
-
if (v.fixed_version) {
|
|
489
|
-
md += `- **Fix:** Upgrade to v${v.fixed_version}\n`;
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
md += `\n`;
|
|
494
|
-
|
|
495
|
-
// Code snippet
|
|
496
|
-
if (v.code_snippet) {
|
|
497
|
-
md += `**Affected code:**\n\`\`\`\n${v.code_snippet}\n\`\`\`\n\n`;
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
// Recommendation
|
|
501
|
-
const rec = v.llm_recommendation || v.recommendation;
|
|
502
|
-
if (rec) {
|
|
503
|
-
md += `**${t('reportRecommendation')}:** ${rec}\n\n`;
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
// Reference
|
|
507
|
-
if (v.reference_url) {
|
|
508
|
-
md += `[Learn more](${v.reference_url})\n\n`;
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
md += `---\n\n`;
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
}
|
|
224
|
+
return report;
|
|
225
|
+
}
|
|
515
226
|
|
|
516
|
-
|
|
517
|
-
|
|
227
|
+
// Interactive prompt
|
|
228
|
+
function prompt(question) {
|
|
229
|
+
const rl = readline.createInterface({
|
|
230
|
+
input: process.stdin,
|
|
231
|
+
output: process.stdout,
|
|
232
|
+
});
|
|
518
233
|
|
|
519
|
-
return
|
|
234
|
+
return new Promise((resolve) => {
|
|
235
|
+
rl.question(question, (answer) => {
|
|
236
|
+
rl.close();
|
|
237
|
+
resolve(answer);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
520
240
|
}
|
|
521
241
|
|
|
522
|
-
//
|
|
523
|
-
async function cmdLogin(args) {
|
|
524
|
-
log(`\n${c.cyan}${c.bold}${t('loginTitle')}${c.reset}\n`);
|
|
242
|
+
// ========== COMMANDS ==========
|
|
525
243
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
log(`
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
const choice = await prompt(t('loginChoice'));
|
|
244
|
+
// Init command
|
|
245
|
+
async function init() {
|
|
246
|
+
console.log(`
|
|
247
|
+
${colors.cyan}${colors.bold}Grepleaks Setup${colors.reset}
|
|
248
|
+
`);
|
|
532
249
|
|
|
533
|
-
if (
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
} else {
|
|
537
|
-
// Email/password login (default)
|
|
538
|
-
await loginWithEmail();
|
|
250
|
+
if (!isGitRepo()) {
|
|
251
|
+
logError('This is not a git repository. Please run "git init" first.');
|
|
252
|
+
process.exit(1);
|
|
539
253
|
}
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
async function loginWithEmail() {
|
|
543
|
-
const email = await prompt(t('loginEmail'));
|
|
544
|
-
const password = await promptPassword(t('loginPassword'));
|
|
545
254
|
|
|
546
|
-
|
|
547
|
-
|
|
255
|
+
// Get API key
|
|
256
|
+
let apiKey = process.env.GREPLEAKS_API_KEY;
|
|
257
|
+
if (!apiKey) {
|
|
258
|
+
console.log(`Get your API key at: ${colors.cyan}${API_URL}${colors.reset} (Settings > API Keys)\n`);
|
|
259
|
+
apiKey = await prompt('Enter your API key: ');
|
|
548
260
|
}
|
|
549
261
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
email,
|
|
554
|
-
password
|
|
555
|
-
});
|
|
556
|
-
|
|
557
|
-
if (res.status !== 200) {
|
|
558
|
-
error(res.data.error || t('loginFailed'));
|
|
262
|
+
if (!apiKey || !apiKey.startsWith('grpl_')) {
|
|
263
|
+
logError('Invalid API key. It should start with "grpl_"');
|
|
264
|
+
process.exit(1);
|
|
559
265
|
}
|
|
560
266
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
email: email
|
|
565
|
-
});
|
|
566
|
-
|
|
567
|
-
success(`${t('loginSuccess')} ${email}`);
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
async function loginWithGitHub() {
|
|
571
|
-
log(`\n${c.cyan}${c.bold}${t('loginGitHubTitle')}${c.reset}\n`);
|
|
572
|
-
const loginUrl = `${WEB_URL}/login?cli=true&provider=github`;
|
|
573
|
-
log(`If your browser doesn't open, visit this URL:`);
|
|
574
|
-
log(`${c.cyan}${loginUrl}${c.reset}\n`);
|
|
575
|
-
info(t('loginOpeningBrowser'));
|
|
576
|
-
openBrowser(loginUrl);
|
|
267
|
+
// Choose when to scan
|
|
268
|
+
console.log(`
|
|
269
|
+
When do you want to run security scans?
|
|
577
270
|
|
|
578
|
-
|
|
579
|
-
|
|
271
|
+
${colors.cyan}1${colors.reset}) On commit (pre-commit hook)
|
|
272
|
+
${colors.cyan}2${colors.reset}) On push (pre-push hook)
|
|
273
|
+
${colors.cyan}3${colors.reset}) Both
|
|
274
|
+
${colors.cyan}4${colors.reset}) Manual only (no hooks)
|
|
275
|
+
`);
|
|
580
276
|
|
|
581
|
-
|
|
582
|
-
|
|
277
|
+
const choice = await prompt('Your choice (1-4): ');
|
|
278
|
+
|
|
279
|
+
// Remove existing hooks
|
|
280
|
+
removeHook('pre-commit');
|
|
281
|
+
removeHook('pre-push');
|
|
282
|
+
|
|
283
|
+
// Create hooks based on choice
|
|
284
|
+
switch (choice) {
|
|
285
|
+
case '1':
|
|
286
|
+
createHook('pre-commit', apiKey);
|
|
287
|
+
logSuccess('Pre-commit hook installed');
|
|
288
|
+
break;
|
|
289
|
+
case '2':
|
|
290
|
+
createHook('pre-push', apiKey);
|
|
291
|
+
logSuccess('Pre-push hook installed');
|
|
292
|
+
break;
|
|
293
|
+
case '3':
|
|
294
|
+
createHook('pre-commit', apiKey);
|
|
295
|
+
createHook('pre-push', apiKey);
|
|
296
|
+
logSuccess('Pre-commit and pre-push hooks installed');
|
|
297
|
+
break;
|
|
298
|
+
case '4':
|
|
299
|
+
logInfo('No hooks installed. Run "npx grepleaks scan" manually.');
|
|
300
|
+
break;
|
|
301
|
+
default:
|
|
302
|
+
logError('Invalid choice');
|
|
303
|
+
process.exit(1);
|
|
583
304
|
}
|
|
584
305
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
306
|
+
// Save config
|
|
307
|
+
saveConfig({
|
|
308
|
+
apiKey: apiKey,
|
|
309
|
+
hook: choice,
|
|
310
|
+
createReport: true,
|
|
588
311
|
});
|
|
589
312
|
|
|
590
|
-
|
|
591
|
-
|
|
313
|
+
// Add config to gitignore
|
|
314
|
+
const gitignorePath = path.join(process.cwd(), '.gitignore');
|
|
315
|
+
let gitignore = '';
|
|
316
|
+
if (fs.existsSync(gitignorePath)) {
|
|
317
|
+
gitignore = fs.readFileSync(gitignorePath, 'utf8');
|
|
318
|
+
}
|
|
319
|
+
if (!gitignore.includes('.grepleaksrc')) {
|
|
320
|
+
fs.appendFileSync(gitignorePath, '\n# Grepleaks\n.grepleaksrc\n.grepleaks-scan.zip\n');
|
|
321
|
+
logInfo('Added .grepleaksrc to .gitignore');
|
|
592
322
|
}
|
|
593
323
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
email: res.data.email || 'GitHub User'
|
|
597
|
-
});
|
|
598
|
-
|
|
599
|
-
success(`${t('loginSuccess')} ${res.data.email || 'GitHub User'}`);
|
|
600
|
-
}
|
|
324
|
+
console.log(`
|
|
325
|
+
${colors.green}${colors.bold}Setup complete!${colors.reset}
|
|
601
326
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
327
|
+
Run a manual scan anytime with:
|
|
328
|
+
${colors.cyan}npx grepleaks scan${colors.reset}
|
|
329
|
+
`);
|
|
605
330
|
}
|
|
606
331
|
|
|
332
|
+
// Scan command
|
|
333
|
+
async function scan(apiKey) {
|
|
334
|
+
const config = getConfig();
|
|
335
|
+
apiKey = apiKey || config.apiKey || process.env.GREPLEAKS_API_KEY;
|
|
607
336
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
if (!creds || !creds.access_token) {
|
|
611
|
-
log(`\n${c.yellow}${t('scanMustLogin')}${c.reset}\n`);
|
|
612
|
-
log(` ${c.cyan}grepleaks login${c.reset}\n`);
|
|
337
|
+
if (!apiKey) {
|
|
338
|
+
logError('No API key provided. Run "npx grepleaks init" or use --api-key');
|
|
613
339
|
process.exit(1);
|
|
614
340
|
}
|
|
615
341
|
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
if (!fs.existsSync(absolutePath)) {
|
|
620
|
-
error(`${t('scanPathNotFound')} ${absolutePath}`);
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
if (!fs.statSync(absolutePath).isDirectory()) {
|
|
624
|
-
error(`${t('scanMustBeDir')} ${absolutePath}`);
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
const projectName = path.basename(absolutePath);
|
|
628
|
-
|
|
629
|
-
log(`\n${c.cyan}${c.bold}grepleaks${c.reset} - ${t('scanTitle')}\n`);
|
|
630
|
-
info(`${t('scanProject')} ${projectName}`);
|
|
631
|
-
info(`${t('scanPath')} ${absolutePath}\n`);
|
|
342
|
+
console.log(`
|
|
343
|
+
${colors.cyan}${colors.bold}Grepleaks Security Scan${colors.reset}
|
|
344
|
+
`);
|
|
632
345
|
|
|
633
|
-
|
|
346
|
+
logInfo('Creating archive...');
|
|
347
|
+
const zipPath = await createZip();
|
|
634
348
|
|
|
349
|
+
logInfo('Scanning...');
|
|
350
|
+
let result;
|
|
635
351
|
try {
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
`${API_URL}/scan/async`,
|
|
643
|
-
tempZip,
|
|
644
|
-
creds.access_token,
|
|
645
|
-
projectName
|
|
646
|
-
);
|
|
647
|
-
|
|
648
|
-
if (uploadRes.status !== 202) {
|
|
649
|
-
error(uploadRes.data.error || t('scanUploadFailed'));
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
const jobId = uploadRes.data.job_id;
|
|
653
|
-
success(`${t('scanStarted')} (job: ${jobId})`);
|
|
654
|
-
|
|
655
|
-
info(t('scanAnalyzing'));
|
|
656
|
-
let result = null;
|
|
657
|
-
let lastProgress = '';
|
|
658
|
-
|
|
659
|
-
while (true) {
|
|
660
|
-
await new Promise(r => setTimeout(r, 2000));
|
|
661
|
-
|
|
662
|
-
const statusRes = await request('GET', `${API_URL}/scan/${jobId}/status`, null, {
|
|
663
|
-
'Authorization': `Bearer ${creds.access_token}`
|
|
664
|
-
});
|
|
665
|
-
|
|
666
|
-
if (statusRes.status !== 200) {
|
|
667
|
-
error(t('scanStatusFailed'));
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
const status = statusRes.data.status;
|
|
671
|
-
const progress = statusRes.data.progress || '';
|
|
672
|
-
|
|
673
|
-
if (progress !== lastProgress) {
|
|
674
|
-
process.stdout.write(`\r${c.gray} ${progress}${c.reset} `);
|
|
675
|
-
lastProgress = progress;
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
if (status === 'completed') {
|
|
679
|
-
result = statusRes.data;
|
|
680
|
-
break;
|
|
681
|
-
} else if (status === 'failed') {
|
|
682
|
-
console.log();
|
|
683
|
-
error(statusRes.data.error || t('scanFailed'));
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
console.log();
|
|
688
|
-
success(`${t('scanComplete')}\n`);
|
|
352
|
+
result = await sendScan(zipPath, apiKey);
|
|
353
|
+
} catch (e) {
|
|
354
|
+
logError(`Scan failed: ${e.message}`);
|
|
355
|
+
fs.unlinkSync(zipPath);
|
|
356
|
+
process.exit(1);
|
|
357
|
+
}
|
|
689
358
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
const score = scanResult.security_score || 0;
|
|
693
|
-
const grade = scanResult.grade_level || 'N/A';
|
|
694
|
-
const vulns = scanResult.vulnerabilities || [];
|
|
359
|
+
// Clean up zip
|
|
360
|
+
fs.unlinkSync(zipPath);
|
|
695
361
|
|
|
696
|
-
|
|
362
|
+
if (result.error) {
|
|
363
|
+
logError(result.error);
|
|
364
|
+
process.exit(1);
|
|
365
|
+
}
|
|
697
366
|
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
367
|
+
// Display results
|
|
368
|
+
console.log(`
|
|
369
|
+
${colors.bold}========================================${colors.reset}
|
|
370
|
+
${colors.bold} SECURITY SCAN RESULTS${colors.reset}
|
|
371
|
+
${colors.bold}========================================${colors.reset}
|
|
701
372
|
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
const high = vulns.filter(v => v.severity?.toUpperCase() === 'HIGH').length;
|
|
705
|
-
const medium = vulns.filter(v => v.severity?.toUpperCase() === 'MEDIUM').length;
|
|
706
|
-
const low = vulns.filter(v => v.severity?.toUpperCase() === 'LOW').length;
|
|
373
|
+
Score: ${colors.bold}${result.security_score || 0}/100${colors.reset}
|
|
374
|
+
Grade: ${colors.bold}${result.grade || 'N/A'}${colors.reset}
|
|
707
375
|
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
}
|
|
376
|
+
Findings:
|
|
377
|
+
Critical: ${colors.red}${result.critical_count || 0}${colors.reset}
|
|
378
|
+
High: ${colors.yellow}${result.high_count || 0}${colors.reset}
|
|
379
|
+
Medium: ${colors.blue}${result.medium_count || 0}${colors.reset}
|
|
380
|
+
Low: ${result.low_count || 0}
|
|
713
381
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
if (scanResult.report_markdown && scanResult.report_markdown.trim().length > 0) {
|
|
717
|
-
report = scanResult.report_markdown;
|
|
718
|
-
log(`\n${c.cyan}ℹ${c.reset} Using AI-generated report from server`);
|
|
719
|
-
} else {
|
|
720
|
-
report = generateMarkdownReport({ ...scanResult, project_name: projectName });
|
|
721
|
-
log(`\n${c.cyan}ℹ${c.reset} Generated report locally (AI report not available)`);
|
|
722
|
-
}
|
|
382
|
+
${colors.bold}========================================${colors.reset}
|
|
383
|
+
`);
|
|
723
384
|
|
|
724
|
-
|
|
385
|
+
// Create report
|
|
386
|
+
if (config.createReport !== false) {
|
|
387
|
+
const report = generateReport(result);
|
|
388
|
+
const reportPath = path.join(process.cwd(), 'SECURITY_REPORT.md');
|
|
725
389
|
fs.writeFileSync(reportPath, report);
|
|
390
|
+
logSuccess(`Report saved to SECURITY_REPORT.md`);
|
|
391
|
+
}
|
|
726
392
|
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
fs.unlinkSync(tempZip);
|
|
732
|
-
}
|
|
393
|
+
// Exit with error if critical findings
|
|
394
|
+
if ((result.critical_count || 0) > 0) {
|
|
395
|
+
logError('Critical vulnerabilities found!');
|
|
396
|
+
process.exit(1);
|
|
733
397
|
}
|
|
398
|
+
|
|
399
|
+
logSuccess('Scan complete!');
|
|
734
400
|
}
|
|
735
401
|
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
402
|
+
// Help command
|
|
403
|
+
function help() {
|
|
404
|
+
console.log(`
|
|
405
|
+
${colors.cyan}${colors.bold}Grepleaks CLI${colors.reset}
|
|
739
406
|
|
|
740
|
-
|
|
741
|
-
grepleaks <command> [options]
|
|
407
|
+
Usage: npx grepleaks <command> [options]
|
|
742
408
|
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
409
|
+
Commands:
|
|
410
|
+
${colors.cyan}init${colors.reset} Setup grepleaks in your project
|
|
411
|
+
${colors.cyan}scan${colors.reset} Run a security scan
|
|
412
|
+
${colors.cyan}help${colors.reset} Show this help message
|
|
747
413
|
|
|
748
|
-
|
|
749
|
-
-
|
|
750
|
-
-v, --version ${t('helpShowVersion')}
|
|
414
|
+
Options:
|
|
415
|
+
${colors.cyan}--api-key${colors.reset} Your Grepleaks API key
|
|
751
416
|
|
|
752
|
-
|
|
753
|
-
grepleaks
|
|
754
|
-
grepleaks scan
|
|
755
|
-
grepleaks scan
|
|
417
|
+
Examples:
|
|
418
|
+
npx grepleaks init
|
|
419
|
+
npx grepleaks scan
|
|
420
|
+
npx grepleaks scan --api-key grpl_xxx
|
|
756
421
|
|
|
757
|
-
${
|
|
758
|
-
${c.cyan}https://grepleaks.com${c.reset}
|
|
422
|
+
Get your API key at: ${colors.cyan}${API_URL}${colors.reset}
|
|
759
423
|
`);
|
|
760
424
|
}
|
|
761
425
|
|
|
762
426
|
// Main
|
|
763
427
|
async function main() {
|
|
764
428
|
const args = process.argv.slice(2);
|
|
765
|
-
|
|
766
|
-
if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
|
|
767
|
-
showHelp();
|
|
768
|
-
process.exit(0);
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
if (args.includes('--version') || args.includes('-v')) {
|
|
772
|
-
log(`${t('version')}${VERSION}`);
|
|
773
|
-
process.exit(0);
|
|
774
|
-
}
|
|
775
|
-
|
|
776
429
|
const command = args[0];
|
|
777
430
|
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
case 'logout':
|
|
784
|
-
await cmdLogout();
|
|
785
|
-
break;
|
|
786
|
-
case 'scan':
|
|
787
|
-
await cmdScan(args);
|
|
788
|
-
break;
|
|
789
|
-
default:
|
|
790
|
-
error(`${t('errorUnknownCmd')} ${command}\n${t('errorUseHelp')}`);
|
|
431
|
+
// Parse options
|
|
432
|
+
let apiKey = null;
|
|
433
|
+
for (let i = 0; i < args.length; i++) {
|
|
434
|
+
if (args[i] === '--api-key' && args[i + 1]) {
|
|
435
|
+
apiKey = args[i + 1];
|
|
791
436
|
}
|
|
792
|
-
}
|
|
793
|
-
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
switch (command) {
|
|
440
|
+
case 'init':
|
|
441
|
+
await init();
|
|
442
|
+
break;
|
|
443
|
+
case 'scan':
|
|
444
|
+
await scan(apiKey);
|
|
445
|
+
break;
|
|
446
|
+
case 'help':
|
|
447
|
+
case '--help':
|
|
448
|
+
case '-h':
|
|
449
|
+
help();
|
|
450
|
+
break;
|
|
451
|
+
default:
|
|
452
|
+
help();
|
|
794
453
|
}
|
|
795
454
|
}
|
|
796
455
|
|
|
797
|
-
main()
|
|
456
|
+
main().catch((e) => {
|
|
457
|
+
logError(e.message);
|
|
458
|
+
process.exit(1);
|
|
459
|
+
});
|
package/package.json
CHANGED
package/README.md
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
# grepleaks
|
|
2
|
-
|
|
3
|
-
Security scanner for your code - detect vulnerabilities, secrets, and misconfigurations.
|
|
4
|
-
|
|
5
|
-
## Installation
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
npm install -g grepleaks
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
## Usage
|
|
12
|
-
|
|
13
|
-
### 1. Login
|
|
14
|
-
|
|
15
|
-
```bash
|
|
16
|
-
# Login with email/password
|
|
17
|
-
grepleaks login
|
|
18
|
-
|
|
19
|
-
# Or login via browser
|
|
20
|
-
grepleaks login --browser
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
### 2. Scan a project
|
|
24
|
-
|
|
25
|
-
```bash
|
|
26
|
-
# Scan current directory
|
|
27
|
-
grepleaks scan .
|
|
28
|
-
|
|
29
|
-
# Scan a specific directory
|
|
30
|
-
grepleaks scan ./my-project
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
### 3. Result
|
|
34
|
-
|
|
35
|
-
The scan generates a `grepleaks-report.md` file in the scanned directory.
|
|
36
|
-
|
|
37
|
-
```
|
|
38
|
-
grepleaks - Security scan
|
|
39
|
-
|
|
40
|
-
→ Project: my-project
|
|
41
|
-
→ Path: /path/to/my-project
|
|
42
|
-
|
|
43
|
-
→ Preparing files...
|
|
44
|
-
✓ Archive created (1.23 MB)
|
|
45
|
-
→ Uploading to server...
|
|
46
|
-
✓ Scan started (job: abc123)
|
|
47
|
-
→ Analyzing...
|
|
48
|
-
✓ Scan complete!
|
|
49
|
-
|
|
50
|
-
Results:
|
|
51
|
-
Score: 75/100 (B)
|
|
52
|
-
Vulnerabilities: 3
|
|
53
|
-
● High: 1
|
|
54
|
-
● Medium: 2
|
|
55
|
-
|
|
56
|
-
✓ Report saved: ./my-project/grepleaks-report.md
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
## Commands
|
|
60
|
-
|
|
61
|
-
| Command | Description |
|
|
62
|
-
|---------|-------------|
|
|
63
|
-
| `grepleaks login` | Login with email/password |
|
|
64
|
-
| `grepleaks login --browser` | Login via browser |
|
|
65
|
-
| `grepleaks logout` | Logout |
|
|
66
|
-
| `grepleaks scan <path>` | Scan a directory |
|
|
67
|
-
| `grepleaks lang <en\|fr>` | Set language |
|
|
68
|
-
| `grepleaks --help` | Show help |
|
|
69
|
-
| `grepleaks --version` | Show version |
|
|
70
|
-
|
|
71
|
-
## Language
|
|
72
|
-
|
|
73
|
-
The CLI is in English by default. To switch to French:
|
|
74
|
-
|
|
75
|
-
```bash
|
|
76
|
-
grepleaks lang fr
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
To switch back to English:
|
|
80
|
-
|
|
81
|
-
```bash
|
|
82
|
-
grepleaks lang en
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
## What it detects
|
|
86
|
-
|
|
87
|
-
- Hardcoded secrets (API keys, passwords, tokens)
|
|
88
|
-
- Vulnerabilities in dependencies
|
|
89
|
-
- Security issues in code (SQL injection, XSS, etc.)
|
|
90
|
-
- Misconfigurations
|
|
91
|
-
|
|
92
|
-
## More info
|
|
93
|
-
|
|
94
|
-
Visit [grepleaks.com](https://grepleaks.com) for the full web interface.
|