grepleaks 1.0.1

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/README.md +94 -0
  2. package/bin/grepleaks.js +822 -0
  3. package/package.json +39 -0
package/README.md ADDED
@@ -0,0 +1,94 @@
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.
@@ -0,0 +1,822 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+ const https = require('https');
7
+ const http = require('http');
8
+ const readline = require('readline');
9
+ const { exec } = require('child_process');
10
+ const archiver = require('archiver');
11
+
12
+ const VERSION = '1.0.1';
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
+ // Translations
20
+ const translations = {
21
+ en: {
22
+ // General
23
+ tagline: 'Security scanner for your code',
24
+ version: 'grepleaks v',
25
+
26
+ // Help
27
+ helpUsage: 'USAGE:',
28
+ helpCommands: 'COMMANDS:',
29
+ helpOptions: 'OPTIONS:',
30
+ helpExamples: 'EXAMPLES:',
31
+ helpMoreInfo: 'MORE INFO:',
32
+ helpLoginDesc: 'Login (email or GitHub)',
33
+ helpLogoutDesc: 'Logout',
34
+ helpScanDesc: 'Scan a directory',
35
+ helpLangDesc: 'Set language (en/fr)',
36
+ helpShowHelp: 'Show help',
37
+ helpShowVersion: 'Show version',
38
+
39
+ // Login
40
+ loginTitle: 'Login to grepleaks',
41
+ loginChooseMethod: 'Choose login method:',
42
+ loginOptionEmail: 'Email / Password',
43
+ loginOptionGitHub: 'GitHub',
44
+ loginChoice: 'Your choice (1 or 2): ',
45
+ loginBrowserTitle: 'Login via browser',
46
+ loginGitHubTitle: 'Login with GitHub',
47
+ loginOpeningBrowser: 'Opening browser...',
48
+ loginTokenPrompt: 'After logging in on the website, copy your token and paste it here.',
49
+ loginTokenInput: 'Token: ',
50
+ loginTokenRequired: 'Token required',
51
+ loginVerifying: 'Verifying token...',
52
+ loginInvalidToken: 'Invalid token',
53
+ loginEmail: 'Email: ',
54
+ loginPassword: 'Password: ',
55
+ loginEmailPasswordRequired: 'Email and password required',
56
+ loginInProgress: 'Logging in...',
57
+ loginFailed: 'Login failed',
58
+ loginSuccess: 'Logged in as',
59
+
60
+ // Logout
61
+ logoutSuccess: 'Logged out',
62
+
63
+ // Scan
64
+ scanTitle: 'Security scan',
65
+ scanMustLogin: 'You must be logged in to scan.',
66
+ scanPathNotFound: 'Path not found:',
67
+ scanMustBeDir: 'Path must be a directory:',
68
+ scanProject: 'Project:',
69
+ scanPath: 'Path:',
70
+ scanPreparing: 'Preparing files...',
71
+ scanArchiveCreated: 'Archive created',
72
+ scanUploading: 'Uploading to server...',
73
+ scanUploadFailed: 'Upload failed',
74
+ scanStarted: 'Scan started',
75
+ scanAnalyzing: 'Analyzing...',
76
+ scanStatusFailed: 'Failed to check status',
77
+ scanFailed: 'Scan failed',
78
+ scanComplete: 'Scan complete!',
79
+ scanResults: 'Results:',
80
+ scanScore: 'Score:',
81
+ scanVulnerabilities: 'Vulnerabilities:',
82
+ scanReportSaved: 'Report saved:',
83
+
84
+ // Report
85
+ reportTitle: 'Security Report',
86
+ reportDate: 'Date:',
87
+ reportScore: 'Score:',
88
+ reportVulns: 'Vulnerabilities:',
89
+ reportNoVulns: 'No vulnerabilities detected',
90
+ reportCongrats: 'Congratulations! Your code has no known vulnerabilities.',
91
+ reportVulnsDetected: 'Vulnerabilities detected',
92
+ reportFile: 'File:',
93
+ reportLine: 'Line:',
94
+ reportScanner: 'Scanner:',
95
+ reportRecommendation: 'Recommendation:',
96
+ reportGeneratedBy: 'Report generated by',
97
+
98
+ // Language
99
+ langSet: 'Language set to',
100
+ langInvalid: 'Invalid language. Use: en, fr',
101
+
102
+ // Errors
103
+ errorUnknownCmd: 'Unknown command:',
104
+ errorUseHelp: "Use 'grepleaks --help' for help."
105
+ },
106
+ fr: {
107
+ // General
108
+ tagline: 'Scanner de securite pour votre code',
109
+ version: 'grepleaks v',
110
+
111
+ // Help
112
+ helpUsage: 'USAGE:',
113
+ helpCommands: 'COMMANDES:',
114
+ helpOptions: 'OPTIONS:',
115
+ helpExamples: 'EXEMPLES:',
116
+ helpMoreInfo: 'PLUS D\'INFO:',
117
+ helpLoginDesc: 'Connexion (email ou GitHub)',
118
+ helpLogoutDesc: 'Deconnexion',
119
+ helpScanDesc: 'Scanner un dossier',
120
+ helpLangDesc: 'Changer la langue (en/fr)',
121
+ helpShowHelp: 'Afficher l\'aide',
122
+ helpShowVersion: 'Afficher la version',
123
+
124
+ // Login
125
+ loginTitle: 'Connexion a grepleaks',
126
+ loginChooseMethod: 'Choisissez votre methode de connexion:',
127
+ loginOptionEmail: 'Email / Mot de passe',
128
+ loginOptionGitHub: 'GitHub',
129
+ loginChoice: 'Votre choix (1 ou 2): ',
130
+ loginBrowserTitle: 'Connexion via navigateur',
131
+ loginGitHubTitle: 'Connexion avec GitHub',
132
+ loginOpeningBrowser: 'Ouverture du navigateur...',
133
+ loginTokenPrompt: 'Apres connexion sur le site, copiez votre token et collez-le ici.',
134
+ loginTokenInput: 'Token: ',
135
+ loginTokenRequired: 'Token requis',
136
+ loginVerifying: 'Verification du token...',
137
+ loginInvalidToken: 'Token invalide',
138
+ loginEmail: 'Email: ',
139
+ loginPassword: 'Mot de passe: ',
140
+ loginEmailPasswordRequired: 'Email et mot de passe requis',
141
+ loginInProgress: 'Connexion en cours...',
142
+ loginFailed: 'Echec de connexion',
143
+ loginSuccess: 'Connecte en tant que',
144
+
145
+ // Logout
146
+ logoutSuccess: 'Deconnecte',
147
+
148
+ // Scan
149
+ scanTitle: 'Scan de securite',
150
+ scanMustLogin: 'Vous devez etre connecte pour scanner.',
151
+ scanPathNotFound: 'Chemin introuvable:',
152
+ scanMustBeDir: 'Le chemin doit etre un dossier:',
153
+ scanProject: 'Projet:',
154
+ scanPath: 'Chemin:',
155
+ scanPreparing: 'Preparation des fichiers...',
156
+ scanArchiveCreated: 'Archive creee',
157
+ scanUploading: 'Envoi au serveur...',
158
+ scanUploadFailed: 'Echec de l\'upload',
159
+ scanStarted: 'Scan demarre',
160
+ scanAnalyzing: 'Analyse en cours...',
161
+ scanStatusFailed: 'Echec de verification du statut',
162
+ scanFailed: 'Le scan a echoue',
163
+ scanComplete: 'Scan termine!',
164
+ scanResults: 'Resultats:',
165
+ scanScore: 'Score:',
166
+ scanVulnerabilities: 'Vulnerabilites:',
167
+ scanReportSaved: 'Rapport sauvegarde:',
168
+
169
+ // Report
170
+ reportTitle: 'Rapport de Securite',
171
+ reportDate: 'Date:',
172
+ reportScore: 'Score:',
173
+ reportVulns: 'Vulnerabilites:',
174
+ reportNoVulns: 'Aucune vulnerabilite detectee',
175
+ reportCongrats: 'Felicitations! Votre code ne presente aucune vulnerabilite connue.',
176
+ reportVulnsDetected: 'Vulnerabilites detectees',
177
+ reportFile: 'Fichier:',
178
+ reportLine: 'Ligne:',
179
+ reportScanner: 'Scanner:',
180
+ reportRecommendation: 'Recommandation:',
181
+ reportGeneratedBy: 'Rapport genere par',
182
+
183
+ // Language
184
+ langSet: 'Langue definie:',
185
+ langInvalid: 'Langue invalide. Utilisez: en, fr',
186
+
187
+ // Errors
188
+ errorUnknownCmd: 'Commande inconnue:',
189
+ errorUseHelp: "Utilisez 'grepleaks --help' pour l'aide."
190
+ }
191
+ };
192
+
193
+ // Files/folders to skip when zipping
194
+ const SKIP_PATTERNS = [
195
+ 'node_modules', '.git', '.svn', '.hg', 'vendor', '__pycache__',
196
+ '.venv', 'venv', 'env', '.env', 'dist', 'build', '.next',
197
+ '.nuxt', 'coverage', '.nyc_output', '.cache', '.parcel-cache',
198
+ 'target', 'Pods', '.gradle', '.idea', '.vscode', '*.log',
199
+ '*.lock', 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml',
200
+ '*.min.js', '*.min.css', '*.map', '*.png', '*.jpg', '*.jpeg',
201
+ '*.gif', '*.ico', '*.svg', '*.woff', '*.woff2', '*.ttf', '*.eot',
202
+ '*.mp3', '*.mp4', '*.avi', '*.mov', '*.pdf', '*.zip', '*.tar',
203
+ '*.gz', '*.rar', '*.7z', '*.exe', '*.dll', '*.so', '*.dylib'
204
+ ];
205
+
206
+ // Colors
207
+ const c = {
208
+ reset: '\x1b[0m',
209
+ bold: '\x1b[1m',
210
+ red: '\x1b[31m',
211
+ green: '\x1b[32m',
212
+ yellow: '\x1b[33m',
213
+ blue: '\x1b[34m',
214
+ cyan: '\x1b[36m',
215
+ gray: '\x1b[90m'
216
+ };
217
+
218
+ // Get current language
219
+ function getLang() {
220
+ const config = loadConfig();
221
+ return config.lang || 'en';
222
+ }
223
+
224
+ // Get translation
225
+ function t(key) {
226
+ const lang = getLang();
227
+ return translations[lang]?.[key] || translations['en'][key] || key;
228
+ }
229
+
230
+ // Helpers
231
+ function log(msg, color = '') {
232
+ console.log(`${color}${msg}${c.reset}`);
233
+ }
234
+
235
+ function error(msg) {
236
+ console.error(`${c.red}Error: ${msg}${c.reset}`);
237
+ process.exit(1);
238
+ }
239
+
240
+ function success(msg) {
241
+ log(`${c.green}✓${c.reset} ${msg}`);
242
+ }
243
+
244
+ function info(msg) {
245
+ log(`${c.cyan}→${c.reset} ${msg}`);
246
+ }
247
+
248
+ // Config management
249
+ function ensureConfigDir() {
250
+ if (!fs.existsSync(CONFIG_DIR)) {
251
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
252
+ }
253
+ }
254
+
255
+ function loadConfig() {
256
+ if (!fs.existsSync(CONFIG_FILE)) {
257
+ return { lang: 'en' };
258
+ }
259
+ try {
260
+ return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
261
+ } catch {
262
+ return { lang: 'en' };
263
+ }
264
+ }
265
+
266
+ function saveConfig(data) {
267
+ ensureConfigDir();
268
+ const current = loadConfig();
269
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify({ ...current, ...data }, null, 2));
270
+ }
271
+
272
+ // Credentials management
273
+ function saveCredentials(data) {
274
+ ensureConfigDir();
275
+ fs.writeFileSync(CREDENTIALS_FILE, JSON.stringify(data, null, 2));
276
+ }
277
+
278
+ function loadCredentials() {
279
+ if (!fs.existsSync(CREDENTIALS_FILE)) {
280
+ return null;
281
+ }
282
+ try {
283
+ return JSON.parse(fs.readFileSync(CREDENTIALS_FILE, 'utf8'));
284
+ } catch {
285
+ return null;
286
+ }
287
+ }
288
+
289
+ function clearCredentials() {
290
+ if (fs.existsSync(CREDENTIALS_FILE)) {
291
+ fs.unlinkSync(CREDENTIALS_FILE);
292
+ }
293
+ }
294
+
295
+ // Readline helper
296
+ function prompt(question) {
297
+ const rl = readline.createInterface({
298
+ input: process.stdin,
299
+ output: process.stdout
300
+ });
301
+ return new Promise(resolve => {
302
+ rl.question(question, answer => {
303
+ rl.close();
304
+ resolve(answer);
305
+ });
306
+ });
307
+ }
308
+
309
+ function promptPassword(question) {
310
+ return new Promise(resolve => {
311
+ const rl = readline.createInterface({
312
+ input: process.stdin,
313
+ output: process.stdout
314
+ });
315
+
316
+ process.stdout.write(question);
317
+
318
+ const stdin = process.stdin;
319
+ const wasRaw = stdin.isRaw;
320
+ if (stdin.setRawMode) stdin.setRawMode(true);
321
+
322
+ let password = '';
323
+
324
+ const onData = (char) => {
325
+ char = char.toString();
326
+
327
+ switch (char) {
328
+ case '\n':
329
+ case '\r':
330
+ case '\u0004':
331
+ if (stdin.setRawMode) stdin.setRawMode(wasRaw);
332
+ stdin.removeListener('data', onData);
333
+ rl.close();
334
+ console.log();
335
+ resolve(password);
336
+ break;
337
+ case '\u0003':
338
+ process.exit();
339
+ break;
340
+ case '\u007F':
341
+ password = password.slice(0, -1);
342
+ break;
343
+ default:
344
+ password += char;
345
+ break;
346
+ }
347
+ };
348
+
349
+ stdin.on('data', onData);
350
+ });
351
+ }
352
+
353
+ // HTTP request helper
354
+ function request(method, url, data = null, headers = {}) {
355
+ return new Promise((resolve, reject) => {
356
+ const urlObj = new URL(url);
357
+ const options = {
358
+ hostname: urlObj.hostname,
359
+ port: urlObj.port || (urlObj.protocol === 'https:' ? 443 : 80),
360
+ path: urlObj.pathname + urlObj.search,
361
+ method,
362
+ headers: {
363
+ 'Content-Type': 'application/json',
364
+ ...headers
365
+ }
366
+ };
367
+
368
+ const proto = urlObj.protocol === 'https:' ? https : http;
369
+ const req = proto.request(options, res => {
370
+ let body = '';
371
+ res.on('data', chunk => body += chunk);
372
+ res.on('end', () => {
373
+ try {
374
+ resolve({ status: res.statusCode, data: JSON.parse(body) });
375
+ } catch {
376
+ resolve({ status: res.statusCode, data: body });
377
+ }
378
+ });
379
+ });
380
+
381
+ req.on('error', reject);
382
+
383
+ if (data) {
384
+ req.write(typeof data === 'string' ? data : JSON.stringify(data));
385
+ }
386
+ req.end();
387
+ });
388
+ }
389
+
390
+ // Upload file with multipart
391
+ function uploadFile(url, filePath, token) {
392
+ return new Promise((resolve, reject) => {
393
+ const boundary = '----grepleaks' + Date.now();
394
+ const fileName = path.basename(filePath);
395
+ const fileStream = fs.createReadStream(filePath);
396
+
397
+ const urlObj = new URL(url);
398
+
399
+ const options = {
400
+ hostname: urlObj.hostname,
401
+ port: urlObj.port || (urlObj.protocol === 'https:' ? 443 : 80),
402
+ path: urlObj.pathname,
403
+ method: 'POST',
404
+ headers: {
405
+ 'Content-Type': `multipart/form-data; boundary=${boundary}`,
406
+ 'Authorization': `Bearer ${token}`
407
+ }
408
+ };
409
+
410
+ const proto = urlObj.protocol === 'https:' ? https : http;
411
+ const req = proto.request(options, res => {
412
+ let body = '';
413
+ res.on('data', chunk => body += chunk);
414
+ res.on('end', () => {
415
+ try {
416
+ resolve({ status: res.statusCode, data: JSON.parse(body) });
417
+ } catch {
418
+ resolve({ status: res.statusCode, data: body });
419
+ }
420
+ });
421
+ });
422
+
423
+ req.on('error', reject);
424
+
425
+ req.write(`--${boundary}\r\n`);
426
+ req.write(`Content-Disposition: form-data; name="file"; filename="${fileName}"\r\n`);
427
+ req.write('Content-Type: application/zip\r\n\r\n');
428
+
429
+ fileStream.on('data', chunk => req.write(chunk));
430
+ fileStream.on('end', () => {
431
+ req.write(`\r\n--${boundary}--\r\n`);
432
+ req.end();
433
+ });
434
+ fileStream.on('error', reject);
435
+ });
436
+ }
437
+
438
+ // Open URL in browser
439
+ function openBrowser(url) {
440
+ const cmd = process.platform === 'darwin' ? 'open' :
441
+ process.platform === 'win32' ? 'start' : 'xdg-open';
442
+ exec(`${cmd} "${url}"`);
443
+ }
444
+
445
+ // Should skip file/folder
446
+ function shouldSkip(name) {
447
+ return SKIP_PATTERNS.some(pattern => {
448
+ if (pattern.startsWith('*.')) {
449
+ return name.endsWith(pattern.slice(1));
450
+ }
451
+ return name === pattern;
452
+ });
453
+ }
454
+
455
+ // Zip directory
456
+ function zipDirectory(sourceDir, outPath) {
457
+ return new Promise((resolve, reject) => {
458
+ const output = fs.createWriteStream(outPath);
459
+ const archive = archiver('zip', { zlib: { level: 6 } });
460
+
461
+ output.on('close', () => resolve(archive.pointer()));
462
+ archive.on('error', reject);
463
+
464
+ archive.pipe(output);
465
+
466
+ const addDir = (dir, prefix = '') => {
467
+ const items = fs.readdirSync(dir);
468
+ for (const item of items) {
469
+ if (shouldSkip(item)) continue;
470
+
471
+ const fullPath = path.join(dir, item);
472
+ const archivePath = prefix ? `${prefix}/${item}` : item;
473
+
474
+ const stat = fs.statSync(fullPath);
475
+ if (stat.isDirectory()) {
476
+ addDir(fullPath, archivePath);
477
+ } else if (stat.size < 1024 * 1024) {
478
+ archive.file(fullPath, { name: archivePath });
479
+ }
480
+ }
481
+ };
482
+
483
+ addDir(sourceDir);
484
+ archive.finalize();
485
+ });
486
+ }
487
+
488
+ // Generate markdown report
489
+ function generateMarkdownReport(result) {
490
+ const vulns = result.vulnerabilities || [];
491
+ const score = result.security_score || 0;
492
+ const grade = result.grade_level || 'N/A';
493
+ const projectName = result.project_name || 'Unknown Project';
494
+
495
+ const dateStr = new Date().toLocaleDateString(getLang() === 'fr' ? 'fr-FR' : 'en-US');
496
+
497
+ let md = `# ${t('reportTitle')} - ${projectName}\n\n`;
498
+ md += `**${t('reportDate')}** ${dateStr}\n`;
499
+ md += `**${t('reportScore')}** ${score}/100 (${grade})\n`;
500
+ md += `**${t('reportVulns')}** ${vulns.length}\n\n`;
501
+
502
+ if (vulns.length === 0) {
503
+ md += `## ${t('reportNoVulns')}\n\n`;
504
+ md += `${t('reportCongrats')}\n`;
505
+ } else {
506
+ md += `## ${t('reportVulnsDetected')}\n\n`;
507
+
508
+ const bySeverity = { CRITICAL: [], HIGH: [], MEDIUM: [], LOW: [] };
509
+ for (const v of vulns) {
510
+ const sev = (v.severity || 'LOW').toUpperCase();
511
+ if (bySeverity[sev]) bySeverity[sev].push(v);
512
+ }
513
+
514
+ for (const [severity, items] of Object.entries(bySeverity)) {
515
+ if (items.length === 0) continue;
516
+
517
+ const emoji = severity === 'CRITICAL' ? '🔴' :
518
+ severity === 'HIGH' ? '🟠' :
519
+ severity === 'MEDIUM' ? '🟡' : '🟢';
520
+
521
+ md += `### ${emoji} ${severity} (${items.length})\n\n`;
522
+
523
+ for (const v of items) {
524
+ md += `#### ${v.title || 'Vulnerability'}\n\n`;
525
+ md += `- **${t('reportFile')}** \`${v.file || 'N/A'}\`\n`;
526
+ if (v.line) md += `- **${t('reportLine')}** ${v.line}\n`;
527
+ md += `- **${t('reportScanner')}** ${v.scanner || 'N/A'}\n`;
528
+ if (v.description) md += `\n${v.description}\n`;
529
+ if (v.recommendation) md += `\n**${t('reportRecommendation')}** ${v.recommendation}\n`;
530
+ md += `\n---\n\n`;
531
+ }
532
+ }
533
+ }
534
+
535
+ md += `\n---\n*${t('reportGeneratedBy')} [grepleaks](https://grepleaks.com)*\n`;
536
+
537
+ return md;
538
+ }
539
+
540
+ // Commands
541
+ async function cmdLogin(args) {
542
+ log(`\n${c.cyan}${c.bold}${t('loginTitle')}${c.reset}\n`);
543
+
544
+ // Show menu
545
+ log(`${t('loginChooseMethod')}\n`);
546
+ log(` ${c.cyan}1.${c.reset} ${t('loginOptionEmail')}`);
547
+ log(` ${c.cyan}2.${c.reset} ${t('loginOptionGitHub')}\n`);
548
+
549
+ const choice = await prompt(t('loginChoice'));
550
+
551
+ if (choice === '2') {
552
+ // GitHub login via browser
553
+ await loginWithGitHub();
554
+ } else {
555
+ // Email/password login (default)
556
+ await loginWithEmail();
557
+ }
558
+ }
559
+
560
+ async function loginWithEmail() {
561
+ const email = await prompt(t('loginEmail'));
562
+ const password = await promptPassword(t('loginPassword'));
563
+
564
+ if (!email || !password) {
565
+ error(t('loginEmailPasswordRequired'));
566
+ }
567
+
568
+ info(t('loginInProgress'));
569
+
570
+ const res = await request('POST', `${API_URL}/auth/signin`, {
571
+ email,
572
+ password
573
+ });
574
+
575
+ if (res.status !== 200) {
576
+ error(res.data.error || t('loginFailed'));
577
+ }
578
+
579
+ saveCredentials({
580
+ access_token: res.data.access_token,
581
+ refresh_token: res.data.refresh_token,
582
+ email: email
583
+ });
584
+
585
+ success(`${t('loginSuccess')} ${email}`);
586
+ }
587
+
588
+ async function loginWithGitHub() {
589
+ log(`\n${c.cyan}${c.bold}${t('loginGitHubTitle')}${c.reset}\n`);
590
+ info(t('loginOpeningBrowser'));
591
+ openBrowser(`${WEB_URL}/login?cli=true&provider=github`);
592
+
593
+ log(`\n${t('loginTokenPrompt')}\n`);
594
+ const token = await prompt(t('loginTokenInput'));
595
+
596
+ if (!token) {
597
+ error(t('loginTokenRequired'));
598
+ }
599
+
600
+ info(t('loginVerifying'));
601
+ const res = await request('GET', `${API_URL}/auth/user`, null, {
602
+ 'Authorization': `Bearer ${token}`
603
+ });
604
+
605
+ if (res.status !== 200) {
606
+ error(t('loginInvalidToken'));
607
+ }
608
+
609
+ saveCredentials({
610
+ access_token: token,
611
+ email: res.data.email || 'GitHub User'
612
+ });
613
+
614
+ success(`${t('loginSuccess')} ${res.data.email || 'GitHub User'}`);
615
+ }
616
+
617
+ async function cmdLogout() {
618
+ clearCredentials();
619
+ success(t('logoutSuccess'));
620
+ }
621
+
622
+ async function cmdLang(args) {
623
+ const lang = args[1];
624
+
625
+ if (!lang) {
626
+ log(`Current language: ${getLang()}`);
627
+ return;
628
+ }
629
+
630
+ if (!['en', 'fr'].includes(lang)) {
631
+ error(t('langInvalid'));
632
+ }
633
+
634
+ saveConfig({ lang });
635
+ success(`${t('langSet')} ${lang}`);
636
+ }
637
+
638
+ async function cmdScan(args) {
639
+ const creds = loadCredentials();
640
+ if (!creds || !creds.access_token) {
641
+ log(`\n${c.yellow}${t('scanMustLogin')}${c.reset}\n`);
642
+ log(` ${c.cyan}grepleaks login${c.reset}\n`);
643
+ process.exit(1);
644
+ }
645
+
646
+ const scanPath = args[1] || '.';
647
+ const absolutePath = path.resolve(process.cwd(), scanPath);
648
+
649
+ if (!fs.existsSync(absolutePath)) {
650
+ error(`${t('scanPathNotFound')} ${absolutePath}`);
651
+ }
652
+
653
+ if (!fs.statSync(absolutePath).isDirectory()) {
654
+ error(`${t('scanMustBeDir')} ${absolutePath}`);
655
+ }
656
+
657
+ const projectName = path.basename(absolutePath);
658
+
659
+ log(`\n${c.cyan}${c.bold}grepleaks${c.reset} - ${t('scanTitle')}\n`);
660
+ info(`${t('scanProject')} ${projectName}`);
661
+ info(`${t('scanPath')} ${absolutePath}\n`);
662
+
663
+ const tempZip = path.join(os.tmpdir(), `grepleaks-${Date.now()}.zip`);
664
+
665
+ try {
666
+ info(t('scanPreparing'));
667
+ const zipSize = await zipDirectory(absolutePath, tempZip);
668
+ success(`${t('scanArchiveCreated')} (${(zipSize / 1024 / 1024).toFixed(2)} MB)`);
669
+
670
+ info(t('scanUploading'));
671
+ const uploadRes = await uploadFile(
672
+ `${API_URL}/scan/async`,
673
+ tempZip,
674
+ creds.access_token
675
+ );
676
+
677
+ if (uploadRes.status !== 202) {
678
+ error(uploadRes.data.error || t('scanUploadFailed'));
679
+ }
680
+
681
+ const jobId = uploadRes.data.job_id;
682
+ success(`${t('scanStarted')} (job: ${jobId})`);
683
+
684
+ info(t('scanAnalyzing'));
685
+ let result = null;
686
+ let lastProgress = '';
687
+
688
+ while (true) {
689
+ await new Promise(r => setTimeout(r, 2000));
690
+
691
+ const statusRes = await request('GET', `${API_URL}/scan/${jobId}/status`, null, {
692
+ 'Authorization': `Bearer ${creds.access_token}`
693
+ });
694
+
695
+ if (statusRes.status !== 200) {
696
+ error(t('scanStatusFailed'));
697
+ }
698
+
699
+ const status = statusRes.data.status;
700
+ const progress = statusRes.data.progress || '';
701
+
702
+ if (progress !== lastProgress) {
703
+ process.stdout.write(`\r${c.gray} ${progress}${c.reset} `);
704
+ lastProgress = progress;
705
+ }
706
+
707
+ if (status === 'completed') {
708
+ result = statusRes.data;
709
+ break;
710
+ } else if (status === 'failed') {
711
+ console.log();
712
+ error(statusRes.data.error || t('scanFailed'));
713
+ }
714
+ }
715
+
716
+ console.log();
717
+ success(`${t('scanComplete')}\n`);
718
+
719
+ // Result data is nested inside result.result from the API response
720
+ const scanResult = result.result || {};
721
+ const score = scanResult.security_score || 0;
722
+ const grade = scanResult.grade_level || 'N/A';
723
+ const vulns = scanResult.vulnerabilities || [];
724
+
725
+ const scoreColor = score >= 80 ? c.green : score >= 50 ? c.yellow : c.red;
726
+
727
+ log(`${c.bold}${t('scanResults')}${c.reset}`);
728
+ log(` ${t('scanScore')} ${scoreColor}${score}/100 (${grade})${c.reset}`);
729
+ log(` ${t('scanVulnerabilities')} ${vulns.length}`);
730
+
731
+ if (vulns.length > 0) {
732
+ const critical = vulns.filter(v => v.severity?.toUpperCase() === 'CRITICAL').length;
733
+ const high = vulns.filter(v => v.severity?.toUpperCase() === 'HIGH').length;
734
+ const medium = vulns.filter(v => v.severity?.toUpperCase() === 'MEDIUM').length;
735
+ const low = vulns.filter(v => v.severity?.toUpperCase() === 'LOW').length;
736
+
737
+ if (critical) log(` ${c.red}● Critical: ${critical}${c.reset}`);
738
+ if (high) log(` ${c.yellow}● High: ${high}${c.reset}`);
739
+ if (medium) log(` ${c.blue}● Medium: ${medium}${c.reset}`);
740
+ if (low) log(` ${c.gray}● Low: ${low}${c.reset}`);
741
+ }
742
+
743
+ const report = generateMarkdownReport({ ...scanResult, project_name: projectName });
744
+ const reportPath = path.join(absolutePath, 'grepleaks-report.md');
745
+ fs.writeFileSync(reportPath, report);
746
+
747
+ log(`\n${c.green}✓${c.reset} ${t('scanReportSaved')} ${c.cyan}${reportPath}${c.reset}\n`);
748
+
749
+ } finally {
750
+ if (fs.existsSync(tempZip)) {
751
+ fs.unlinkSync(tempZip);
752
+ }
753
+ }
754
+ }
755
+
756
+ function showHelp() {
757
+ log(`
758
+ ${c.bold}${c.cyan}grepleaks${c.reset} - ${t('tagline')}
759
+
760
+ ${c.bold}${t('helpUsage')}${c.reset}
761
+ grepleaks <command> [options]
762
+
763
+ ${c.bold}${t('helpCommands')}${c.reset}
764
+ login ${t('helpLoginDesc')}
765
+ logout ${t('helpLogoutDesc')}
766
+ scan <path> ${t('helpScanDesc')}
767
+ lang <en|fr> ${t('helpLangDesc')}
768
+
769
+ ${c.bold}${t('helpOptions')}${c.reset}
770
+ -h, --help ${t('helpShowHelp')}
771
+ -v, --version ${t('helpShowVersion')}
772
+
773
+ ${c.bold}${t('helpExamples')}${c.reset}
774
+ grepleaks login
775
+ grepleaks scan .
776
+ grepleaks scan ./my-project
777
+ grepleaks lang fr
778
+
779
+ ${c.bold}${t('helpMoreInfo')}${c.reset}
780
+ ${c.cyan}https://grepleaks.com${c.reset}
781
+ `);
782
+ }
783
+
784
+ // Main
785
+ async function main() {
786
+ const args = process.argv.slice(2);
787
+
788
+ if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
789
+ showHelp();
790
+ process.exit(0);
791
+ }
792
+
793
+ if (args.includes('--version') || args.includes('-v')) {
794
+ log(`${t('version')}${VERSION}`);
795
+ process.exit(0);
796
+ }
797
+
798
+ const command = args[0];
799
+
800
+ try {
801
+ switch (command) {
802
+ case 'login':
803
+ await cmdLogin(args);
804
+ break;
805
+ case 'logout':
806
+ await cmdLogout();
807
+ break;
808
+ case 'lang':
809
+ await cmdLang(args);
810
+ break;
811
+ case 'scan':
812
+ await cmdScan(args);
813
+ break;
814
+ default:
815
+ error(`${t('errorUnknownCmd')} ${command}\n${t('errorUseHelp')}`);
816
+ }
817
+ } catch (err) {
818
+ error(err.message);
819
+ }
820
+ }
821
+
822
+ main();
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "grepleaks",
3
+ "version": "1.0.1",
4
+ "description": "Security scanner for your code - detect vulnerabilities, secrets, and misconfigurations",
5
+ "main": "bin/grepleaks.js",
6
+ "bin": {
7
+ "grepleaks": "./bin/grepleaks.js"
8
+ },
9
+ "scripts": {
10
+ "test": "node bin/grepleaks.js --help"
11
+ },
12
+ "keywords": [
13
+ "security",
14
+ "scanner",
15
+ "vulnerability",
16
+ "secrets",
17
+ "sast",
18
+ "devsecops",
19
+ "trivy",
20
+ "semgrep",
21
+ "code-analysis"
22
+ ],
23
+ "author": "Lucas Bolens",
24
+ "license": "MIT",
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "https://github.com/lbolens/grepleaks.git"
28
+ },
29
+ "homepage": "https://grepleaks.com",
30
+ "engines": {
31
+ "node": ">=14.0.0"
32
+ },
33
+ "files": [
34
+ "bin/"
35
+ ],
36
+ "dependencies": {
37
+ "archiver": "^6.0.1"
38
+ }
39
+ }