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.
Files changed (3) hide show
  1. package/bin/grepleaks.js +327 -665
  2. package/package.json +1 -1
  3. package/README.md +0 -94
package/bin/grepleaks.js CHANGED
@@ -1,797 +1,459 @@
1
- #!/usr/bin/env -S node --no-deprecation
1
+ #!/usr/bin/env node
2
2
 
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
- const os = require('os');
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 VERSION = '1.2.4';
13
- const API_URL = 'https://grepleaks.com/api/v1';
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
- // Files/folders to skip when zipping
95
- const SKIP_PATTERNS = [
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
- gray: '\x1b[90m'
22
+ bold: '\x1b[1m',
117
23
  };
118
24
 
119
- // Get text string
120
- function t(key) {
121
- return TEXT[key] || key;
25
+ function log(message, color = '') {
26
+ console.log(`${color}${message}${colors.reset}`);
122
27
  }
123
28
 
124
- // Helpers
125
- function log(msg, color = '') {
126
- console.log(`${color}${msg}${c.reset}`);
29
+ function logError(message) {
30
+ log(`Error: ${message}`, colors.red);
127
31
  }
128
32
 
129
- function error(msg) {
130
- console.error(`${c.red}Error: ${msg}${c.reset}`);
131
- process.exit(1);
33
+ function logSuccess(message) {
34
+ log(`${message}`, colors.green);
132
35
  }
133
36
 
134
- function success(msg) {
135
- log(`${c.green}✓${c.reset} ${msg}`);
37
+ function logInfo(message) {
38
+ log(`${message}`, colors.cyan);
136
39
  }
137
40
 
138
- function info(msg) {
139
- log(`${c.cyan}→${c.reset} ${msg}`);
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
- // Config management
143
- function ensureConfigDir() {
144
- if (!fs.existsSync(CONFIG_DIR)) {
145
- fs.mkdirSync(CONFIG_DIR, { recursive: true });
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
- function loadConfig() {
150
- if (!fs.existsSync(CONFIG_FILE)) {
151
- return { lang: 'en' };
152
- }
60
+ // Check if inside a git repo
61
+ function isGitRepo() {
153
62
  try {
154
- return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
155
- } catch {
156
- return { lang: 'en' };
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
- function saveConfig(data) {
161
- ensureConfigDir();
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
- return JSON.parse(fs.readFileSync(CREDENTIALS_FILE, 'utf8'));
178
- } catch {
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
- function clearCredentials() {
184
- if (fs.existsSync(CREDENTIALS_FILE)) {
185
- fs.unlinkSync(CREDENTIALS_FILE);
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
- // Readline helper
190
- function prompt(question) {
191
- const rl = readline.createInterface({
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
- function promptPassword(question) {
204
- return new Promise(resolve => {
205
- const rl = readline.createInterface({
206
- input: process.stdin,
207
- output: process.stdout
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
- process.stdout.write(question);
211
-
212
- const stdin = process.stdin;
213
- const wasRaw = stdin.isRaw;
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
- stdin.on('data', onData);
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
- // HTTP request helper
248
- function request(method, url, data = null, headers = {}) {
117
+ // Create zip of current directory
118
+ async function createZip() {
249
119
  return new Promise((resolve, reject) => {
250
- const urlObj = new URL(url);
251
- const options = {
252
- hostname: urlObj.hostname,
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
- const proto = urlObj.protocol === 'https:' ? https : http;
263
- const req = proto.request(options, res => {
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
- req.on('error', reject);
127
+ archive.pipe(output);
276
128
 
277
- if (data) {
278
- req.write(typeof data === 'string' ? data : JSON.stringify(data));
279
- }
280
- req.end();
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
- // Upload file with multipart
285
- function uploadFile(url, filePath, token, projectName) {
139
+ // Send scan request to API
140
+ async function sendScan(zipPath, apiKey) {
286
141
  return new Promise((resolve, reject) => {
287
- const boundary = '----grepleaks' + Date.now();
288
- const fileName = path.basename(filePath);
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 urlObj = new URL(url);
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: urlObj.hostname,
295
- port: urlObj.port || (urlObj.protocol === 'https:' ? 443 : 80),
296
- path: urlObj.pathname,
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
- 'Authorization': `Bearer ${token}`
301
- }
165
+ 'Content-Length': body.length,
166
+ },
302
167
  };
303
168
 
304
- const proto = urlObj.protocol === 'https:' ? https : http;
305
- const req = proto.request(options, res => {
306
- let body = '';
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({ status: res.statusCode, data: JSON.parse(body) });
311
- } catch {
312
- resolve({ status: res.statusCode, data: body });
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
- // Add project_name field
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 generateMarkdownReport(result) {
397
- const vulns = result.vulnerabilities || [];
398
- const score = result.security_score || 0;
399
- const grade = result.grade_level || 'N/A';
400
- const projectName = result.project_name || 'Unknown Project';
401
-
402
- const dateStr = new Date().toLocaleDateString('en-US', {
403
- year: 'numeric',
404
- month: 'long',
405
- day: 'numeric'
406
- });
407
-
408
- // Count by severity
409
- const counts = { critical: 0, high: 0, medium: 0, low: 0 };
410
- for (const v of vulns) {
411
- const sev = (v.severity || 'low').toLowerCase();
412
- if (counts[sev] !== undefined) counts[sev]++;
413
- }
414
-
415
- // Header
416
- let md = `# ${t('reportTitle')}\n\n`;
417
- md += `> **${projectName}** | ${dateStr}\n\n`;
418
-
419
- // Score card
420
- const scoreEmoji = score >= 90 ? '🛡️' : score >= 75 ? '✅' : score >= 50 ? '⚠️' : '🚨';
421
- md += `## ${scoreEmoji} Security Score: ${score}/100 (${grade})\n\n`;
422
-
423
- // Summary table
424
- if (vulns.length > 0) {
425
- md += `| Severity | Count |\n`;
426
- md += `|----------|-------|\n`;
427
- if (counts.critical > 0) md += `| 🔴 Critical | ${counts.critical} |\n`;
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
- // Vulnerabilities
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
- const emoji = severity === 'CRITICAL' ? '🔴' :
452
- severity === 'HIGH' ? '🟠' :
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
- // Footer
517
- md += `*${t('reportGeneratedBy')} [grepleaks](https://grepleaks.com)*\n`;
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 md;
234
+ return new Promise((resolve) => {
235
+ rl.question(question, (answer) => {
236
+ rl.close();
237
+ resolve(answer);
238
+ });
239
+ });
520
240
  }
521
241
 
522
- // Commands
523
- async function cmdLogin(args) {
524
- log(`\n${c.cyan}${c.bold}${t('loginTitle')}${c.reset}\n`);
242
+ // ========== COMMANDS ==========
525
243
 
526
- // Show menu
527
- log(`${t('loginChooseMethod')}\n`);
528
- log(` ${c.cyan}1.${c.reset} ${t('loginOptionEmail')}`);
529
- log(` ${c.cyan}2.${c.reset} ${t('loginOptionGitHub')}\n`);
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 (choice === '2') {
534
- // GitHub login via browser
535
- await loginWithGitHub();
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
- if (!email || !password) {
547
- error(t('loginEmailPasswordRequired'));
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
- info(t('loginInProgress'));
551
-
552
- const res = await request('POST', `${API_URL}/auth/signin`, {
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
- saveCredentials({
562
- access_token: res.data.access_token,
563
- refresh_token: res.data.refresh_token,
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
- log(`\n${t('loginTokenPrompt')}\n`);
579
- const token = await prompt(t('loginTokenInput'));
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
- if (!token) {
582
- error(t('loginTokenRequired'));
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
- info(t('loginVerifying'));
586
- const res = await request('GET', `${API_URL}/auth/user`, null, {
587
- 'Authorization': `Bearer ${token}`
306
+ // Save config
307
+ saveConfig({
308
+ apiKey: apiKey,
309
+ hook: choice,
310
+ createReport: true,
588
311
  });
589
312
 
590
- if (res.status !== 200) {
591
- error(t('loginInvalidToken'));
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
- saveCredentials({
595
- access_token: token,
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
- async function cmdLogout() {
603
- clearCredentials();
604
- success(t('logoutSuccess'));
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
- async function cmdScan(args) {
609
- const creds = loadCredentials();
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
- const scanPath = args[1] || '.';
617
- const absolutePath = path.resolve(process.cwd(), scanPath);
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
- const tempZip = path.join(os.tmpdir(), `grepleaks-${Date.now()}.zip`);
346
+ logInfo('Creating archive...');
347
+ const zipPath = await createZip();
634
348
 
349
+ logInfo('Scanning...');
350
+ let result;
635
351
  try {
636
- info(t('scanPreparing'));
637
- const zipSize = await zipDirectory(absolutePath, tempZip);
638
- success(`${t('scanArchiveCreated')} (${(zipSize / 1024 / 1024).toFixed(2)} MB)`);
639
-
640
- info(t('scanUploading'));
641
- const uploadRes = await uploadFile(
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
- // Result data is nested inside result.result from the API response
691
- const scanResult = result.result || {};
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
- const scoreColor = score >= 80 ? c.green : score >= 50 ? c.yellow : c.red;
362
+ if (result.error) {
363
+ logError(result.error);
364
+ process.exit(1);
365
+ }
697
366
 
698
- log(`${c.bold}${t('scanResults')}${c.reset}`);
699
- log(` ${t('scanScore')} ${scoreColor}${score}/100 (${grade})${c.reset}`);
700
- log(` ${t('scanVulnerabilities')} ${vulns.length}`);
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
- if (vulns.length > 0) {
703
- const critical = vulns.filter(v => v.severity?.toUpperCase() === 'CRITICAL').length;
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
- if (critical) log(` ${c.red}● Critical: ${critical}${c.reset}`);
709
- if (high) log(` ${c.yellow} High: ${high}${c.reset}`);
710
- if (medium) log(` ${c.blue} Medium: ${medium}${c.reset}`);
711
- if (low) log(` ${c.gray} Low: ${low}${c.reset}`);
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
- // Use AI-generated report from backend if available, otherwise generate locally
715
- let report;
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
- const reportPath = path.join(absolutePath, 'grepleaks-report.md');
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
- log(`${c.green}✓${c.reset} ${t('scanReportSaved')} ${c.cyan}${reportPath}${c.reset}\n`);
728
-
729
- } finally {
730
- if (fs.existsSync(tempZip)) {
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
- function showHelp() {
737
- log(`
738
- ${c.bold}${c.cyan}grepleaks${c.reset} - ${t('tagline')}
402
+ // Help command
403
+ function help() {
404
+ console.log(`
405
+ ${colors.cyan}${colors.bold}Grepleaks CLI${colors.reset}
739
406
 
740
- ${c.bold}${t('helpUsage')}${c.reset}
741
- grepleaks <command> [options]
407
+ Usage: npx grepleaks <command> [options]
742
408
 
743
- ${c.bold}${t('helpCommands')}${c.reset}
744
- login ${t('helpLoginDesc')}
745
- logout ${t('helpLogoutDesc')}
746
- scan <path> ${t('helpScanDesc')}
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
- ${c.bold}${t('helpOptions')}${c.reset}
749
- -h, --help ${t('helpShowHelp')}
750
- -v, --version ${t('helpShowVersion')}
414
+ Options:
415
+ ${colors.cyan}--api-key${colors.reset} Your Grepleaks API key
751
416
 
752
- ${c.bold}${t('helpExamples')}${c.reset}
753
- grepleaks login
754
- grepleaks scan .
755
- grepleaks scan ./my-project
417
+ Examples:
418
+ npx grepleaks init
419
+ npx grepleaks scan
420
+ npx grepleaks scan --api-key grpl_xxx
756
421
 
757
- ${c.bold}${t('helpMoreInfo')}${c.reset}
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
- try {
779
- switch (command) {
780
- case 'login':
781
- await cmdLogin(args);
782
- break;
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
- } catch (err) {
793
- error(err.message);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepleaks",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
4
4
  "description": "Security scanner for your code - detect vulnerabilities, secrets, and misconfigurations",
5
5
  "main": "bin/grepleaks.js",
6
6
  "bin": {
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.