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.
- package/README.md +94 -0
- package/bin/grepleaks.js +822 -0
- 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.
|
package/bin/grepleaks.js
ADDED
|
@@ -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
|
+
}
|