vibe-shield 1.0.0 → 1.1.0
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/dist/cli.js +246 -47
- package/dist/index.d.ts +2 -2
- package/dist/index.js +198 -32
- package/dist/prompter.d.ts +10 -2
- package/dist/scanner.d.ts +2 -2
- package/dist/types.d.ts +7 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,130 +1,227 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/scanner.ts
|
|
4
|
-
import { readFileSync, readdirSync, statSync } from "fs";
|
|
4
|
+
import { readFileSync, readdirSync, statSync, existsSync } from "fs";
|
|
5
5
|
import { join, extname } from "path";
|
|
6
6
|
|
|
7
7
|
// src/patterns.ts
|
|
8
8
|
var securityPatterns = [
|
|
9
|
+
{
|
|
10
|
+
id: "aws-access-key",
|
|
11
|
+
name: "AWS Access Key",
|
|
12
|
+
regex: /['"`](AKIA[0-9A-Z]{16})['"`]/g,
|
|
13
|
+
fixPrompt: "AWS access key detected. Remove immediately and rotate in AWS console. Use environment variables or IAM roles.",
|
|
14
|
+
severity: "critical"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
id: "aws-secret-key",
|
|
18
|
+
name: "AWS Secret Key",
|
|
19
|
+
regex: /aws_?secret_?access_?key\s*[:=]\s*['"`][A-Za-z0-9\/+=]{40}['"`]/gi,
|
|
20
|
+
fixPrompt: "AWS secret key detected. Remove immediately and rotate in AWS console. Use environment variables.",
|
|
21
|
+
severity: "critical"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: "openai-key",
|
|
25
|
+
name: "OpenAI API Key",
|
|
26
|
+
regex: /['"`](sk-[A-Za-z0-9]{48,})['"`]/g,
|
|
27
|
+
fixPrompt: "OpenAI API key detected. Remove and rotate at platform.openai.com. Use process.env.OPENAI_API_KEY.",
|
|
28
|
+
severity: "critical"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: "anthropic-key",
|
|
32
|
+
name: "Anthropic API Key",
|
|
33
|
+
regex: /['"`](sk-ant-[A-Za-z0-9\-]{80,})['"`]/g,
|
|
34
|
+
fixPrompt: "Anthropic API key detected. Remove and rotate at console.anthropic.com. Use process.env.ANTHROPIC_API_KEY.",
|
|
35
|
+
severity: "critical"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: "stripe-secret",
|
|
39
|
+
name: "Stripe Secret Key",
|
|
40
|
+
regex: /['"`](sk_live_[A-Za-z0-9]{24,})['"`]/g,
|
|
41
|
+
fixPrompt: "Stripe live secret key detected. Remove and rotate in Stripe dashboard. Use process.env.STRIPE_SECRET_KEY.",
|
|
42
|
+
severity: "critical"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: "stripe-restricted",
|
|
46
|
+
name: "Stripe Restricted Key",
|
|
47
|
+
regex: /['"`](rk_live_[A-Za-z0-9]{24,})['"`]/g,
|
|
48
|
+
fixPrompt: "Stripe restricted key detected. Remove and rotate in Stripe dashboard. Use environment variables.",
|
|
49
|
+
severity: "critical"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: "github-token",
|
|
53
|
+
name: "GitHub Token",
|
|
54
|
+
regex: /['"`](ghp_[A-Za-z0-9]{36}|github_pat_[A-Za-z0-9_]{22,})['"`]/g,
|
|
55
|
+
fixPrompt: "GitHub token detected. Remove and rotate at github.com/settings/tokens. Use process.env.GITHUB_TOKEN.",
|
|
56
|
+
severity: "critical"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: "slack-token",
|
|
60
|
+
name: "Slack Token",
|
|
61
|
+
regex: /['"`](xox[baprs]-[A-Za-z0-9\-]{10,})['"`]/g,
|
|
62
|
+
fixPrompt: "Slack token detected. Remove and rotate in Slack app settings. Use environment variables.",
|
|
63
|
+
severity: "critical"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: "twilio-key",
|
|
67
|
+
name: "Twilio API Key",
|
|
68
|
+
regex: /['"`](SK[A-Za-z0-9]{32})['"`]/g,
|
|
69
|
+
fixPrompt: "Twilio API key detected. Remove and rotate in Twilio console. Use environment variables.",
|
|
70
|
+
severity: "critical"
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: "sendgrid-key",
|
|
74
|
+
name: "SendGrid API Key",
|
|
75
|
+
regex: /['"`](SG\.[A-Za-z0-9\-_]{22}\.[A-Za-z0-9\-_]{43})['"`]/g,
|
|
76
|
+
fixPrompt: "SendGrid API key detected. Remove and rotate in SendGrid dashboard. Use environment variables.",
|
|
77
|
+
severity: "critical"
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: "private-key",
|
|
81
|
+
name: "Private Key",
|
|
82
|
+
regex: /-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/g,
|
|
83
|
+
fixPrompt: "Private key detected in code. Move to a secure file outside repo or use a secrets manager.",
|
|
84
|
+
severity: "critical"
|
|
85
|
+
},
|
|
9
86
|
{
|
|
10
87
|
id: "hardcoded-secret",
|
|
11
88
|
name: "Hardcoded Secret",
|
|
12
89
|
regex: /(?:api_?key|api_?secret|secret_?key|auth_?token|access_?token|private_?key|client_?secret)\s*[:=]\s*['"`][A-Za-z0-9_\-\.\/\+]{8,}['"`]/gi,
|
|
13
|
-
fixPrompt: "Move this secret to an environment variable. Add
|
|
90
|
+
fixPrompt: "Move this secret to an environment variable. Add to .env and use process.env.YOUR_SECRET. Add .env to .gitignore.",
|
|
91
|
+
severity: "high"
|
|
14
92
|
},
|
|
15
93
|
{
|
|
16
94
|
id: "hardcoded-password",
|
|
17
95
|
name: "Hardcoded Password",
|
|
18
96
|
regex: /(?:password|passwd|pwd)\s*[:=]\s*['"`][^'"`\s]{4,}['"`]/gi,
|
|
19
|
-
fixPrompt: "Move this password to an environment variable. Never commit passwords to version control."
|
|
20
|
-
|
|
21
|
-
{
|
|
22
|
-
id: "aws-key",
|
|
23
|
-
name: "AWS Access Key",
|
|
24
|
-
regex: /['"`](AKIA[0-9A-Z]{16})['"`]/g,
|
|
25
|
-
fixPrompt: "AWS access key detected. Remove it immediately and rotate the key in AWS console. Use environment variables or AWS IAM roles."
|
|
97
|
+
fixPrompt: "Move this password to an environment variable. Never commit passwords to version control.",
|
|
98
|
+
severity: "high"
|
|
26
99
|
},
|
|
27
100
|
{
|
|
28
101
|
id: "jwt-secret-inline",
|
|
29
102
|
name: "Hardcoded JWT Secret",
|
|
30
103
|
regex: /jwt\.sign\s*\([^)]+,\s*['"`][^'"`]{8,}['"`]/gi,
|
|
31
|
-
fixPrompt: "Move JWT secret to an environment variable. Hardcoded secrets get committed to version control."
|
|
104
|
+
fixPrompt: "Move JWT secret to an environment variable. Hardcoded secrets get committed to version control.",
|
|
105
|
+
severity: "high"
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
id: "database-url",
|
|
109
|
+
name: "Database Connection String",
|
|
110
|
+
regex: /['"`](mongodb(\+srv)?|postgres(ql)?|mysql|redis):\/\/[^'"`\s]{10,}['"`]/gi,
|
|
111
|
+
fixPrompt: "Database connection string with credentials detected. Use process.env.DATABASE_URL.",
|
|
112
|
+
severity: "high"
|
|
32
113
|
},
|
|
33
114
|
{
|
|
34
115
|
id: "sql-injection-template",
|
|
35
116
|
name: "SQL Injection",
|
|
36
117
|
regex: /(?:query|execute)\s*\(\s*`(?:SELECT|INSERT|UPDATE|DELETE|DROP)[^`]*\$\{/gi,
|
|
37
|
-
fixPrompt: "Use parameterized queries instead of template literals. Example: query('SELECT * FROM users WHERE id = ?', [userId])"
|
|
118
|
+
fixPrompt: "Use parameterized queries instead of template literals. Example: query('SELECT * FROM users WHERE id = ?', [userId])",
|
|
119
|
+
severity: "high"
|
|
38
120
|
},
|
|
39
121
|
{
|
|
40
122
|
id: "sql-injection-concat",
|
|
41
123
|
name: "SQL Injection",
|
|
42
124
|
regex: /(?:query|execute)\s*\(\s*['"](?:SELECT|INSERT|UPDATE|DELETE)[^'"]*['"]\s*\+/gi,
|
|
43
|
-
fixPrompt: "Never concatenate variables into SQL strings. Use parameterized queries with placeholders."
|
|
125
|
+
fixPrompt: "Never concatenate variables into SQL strings. Use parameterized queries with placeholders.",
|
|
126
|
+
severity: "high"
|
|
44
127
|
},
|
|
45
128
|
{
|
|
46
129
|
id: "command-injection",
|
|
47
130
|
name: "Command Injection",
|
|
48
131
|
regex: /(?:exec|execSync)\s*\(\s*`[^`]*\$\{/g,
|
|
49
|
-
fixPrompt: "Avoid template literals in shell commands. Use spawn() with an array of arguments
|
|
132
|
+
fixPrompt: "Avoid template literals in shell commands. Use spawn() with an array of arguments.",
|
|
133
|
+
severity: "critical"
|
|
50
134
|
},
|
|
51
135
|
{
|
|
52
136
|
id: "command-injection-concat",
|
|
53
137
|
name: "Command Injection",
|
|
54
138
|
regex: /(?:exec|execSync)\s*\([^)]*\+\s*(?:req\.|user|input|param|query|body)/gi,
|
|
55
|
-
fixPrompt: "User input in shell commands allows arbitrary command execution. Use spawn() with argument arrays."
|
|
139
|
+
fixPrompt: "User input in shell commands allows arbitrary command execution. Use spawn() with argument arrays.",
|
|
140
|
+
severity: "critical"
|
|
56
141
|
},
|
|
57
142
|
{
|
|
58
143
|
id: "eval-usage",
|
|
59
144
|
name: "Dangerous eval()",
|
|
60
145
|
regex: /[=:]\s*eval\s*\(\s*(?:req\.|user|input|param|query|body|data)/gi,
|
|
61
|
-
fixPrompt: "eval() with user input allows arbitrary code execution. Use JSON.parse() for JSON, or refactor to avoid eval
|
|
146
|
+
fixPrompt: "eval() with user input allows arbitrary code execution. Use JSON.parse() for JSON, or refactor to avoid eval.",
|
|
147
|
+
severity: "high"
|
|
62
148
|
},
|
|
63
149
|
{
|
|
64
150
|
id: "new-function",
|
|
65
151
|
name: "Dangerous Function Constructor",
|
|
66
152
|
regex: /new\s+Function\s*\([^)]*(?:req\.|user|input|param|query|body)/gi,
|
|
67
|
-
fixPrompt: "new Function() with user input is as dangerous as eval(). Refactor to avoid dynamic code generation."
|
|
153
|
+
fixPrompt: "new Function() with user input is as dangerous as eval(). Refactor to avoid dynamic code generation.",
|
|
154
|
+
severity: "high"
|
|
68
155
|
},
|
|
69
156
|
{
|
|
70
157
|
id: "innerhtml-variable",
|
|
71
158
|
name: "XSS via innerHTML",
|
|
72
159
|
regex: /\.innerHTML\s*=\s*(?:req\.|user|input|param|query|body|data|props\.|this\.)/gi,
|
|
73
|
-
fixPrompt: "Setting innerHTML with user data enables XSS attacks. Use textContent or sanitize with DOMPurify."
|
|
160
|
+
fixPrompt: "Setting innerHTML with user data enables XSS attacks. Use textContent or sanitize with DOMPurify.",
|
|
161
|
+
severity: "high"
|
|
74
162
|
},
|
|
75
163
|
{
|
|
76
164
|
id: "react-dangerous-html",
|
|
77
165
|
name: "React XSS Risk",
|
|
78
166
|
regex: /dangerouslySetInnerHTML\s*=\s*\{\s*\{\s*__html\s*:\s*(?:props\.|this\.|data|user|input)/gi,
|
|
79
|
-
fixPrompt: "dangerouslySetInnerHTML with user data enables XSS. Sanitize HTML with DOMPurify first."
|
|
167
|
+
fixPrompt: "dangerouslySetInnerHTML with user data enables XSS. Sanitize HTML with DOMPurify first.",
|
|
168
|
+
severity: "high"
|
|
80
169
|
},
|
|
81
170
|
{
|
|
82
171
|
id: "weak-hash-md5",
|
|
83
172
|
name: "Weak Hash (MD5)",
|
|
84
173
|
regex: /createHash\s*\(\s*['"`]md5['"`]\s*\)/g,
|
|
85
|
-
fixPrompt: "MD5 is broken. Use crypto.createHash('sha256'). For passwords, use bcrypt or argon2."
|
|
174
|
+
fixPrompt: "MD5 is broken. Use crypto.createHash('sha256'). For passwords, use bcrypt or argon2.",
|
|
175
|
+
severity: "medium"
|
|
86
176
|
},
|
|
87
177
|
{
|
|
88
178
|
id: "weak-hash-sha1",
|
|
89
179
|
name: "Weak Hash (SHA1)",
|
|
90
180
|
regex: /createHash\s*\(\s*['"`]sha1['"`]\s*\)/g,
|
|
91
|
-
fixPrompt: "SHA1 is deprecated for security. Use crypto.createHash('sha256'). For passwords, use bcrypt or argon2."
|
|
181
|
+
fixPrompt: "SHA1 is deprecated for security. Use crypto.createHash('sha256'). For passwords, use bcrypt or argon2.",
|
|
182
|
+
severity: "medium"
|
|
92
183
|
},
|
|
93
184
|
{
|
|
94
185
|
id: "ssl-disabled",
|
|
95
186
|
name: "SSL Verification Disabled",
|
|
96
187
|
regex: /rejectUnauthorized\s*:\s*false/g,
|
|
97
|
-
fixPrompt: "Disabling SSL verification allows man-in-the-middle attacks. Remove this or set to true."
|
|
188
|
+
fixPrompt: "Disabling SSL verification allows man-in-the-middle attacks. Remove this or set to true.",
|
|
189
|
+
severity: "medium"
|
|
98
190
|
},
|
|
99
191
|
{
|
|
100
192
|
id: "cors-wildcard",
|
|
101
193
|
name: "CORS Allows All Origins",
|
|
102
194
|
regex: /['"`]Access-Control-Allow-Origin['"`]\s*[,:]\s*['"`]\*['"`]/g,
|
|
103
|
-
fixPrompt: "CORS wildcard (*) allows any website to make requests. Specify allowed origins explicitly."
|
|
195
|
+
fixPrompt: "CORS wildcard (*) allows any website to make requests. Specify allowed origins explicitly.",
|
|
196
|
+
severity: "medium"
|
|
104
197
|
},
|
|
105
198
|
{
|
|
106
199
|
id: "path-traversal",
|
|
107
200
|
name: "Path Traversal Risk",
|
|
108
201
|
regex: /(?:readFile|writeFile|readFileSync|writeFileSync)\s*\(\s*(?:req\.|user|input|param|query|body)/gi,
|
|
109
|
-
fixPrompt: "User input in file paths allows reading/writing arbitrary files. Validate paths with path.resolve() and check they're within allowed directories."
|
|
202
|
+
fixPrompt: "User input in file paths allows reading/writing arbitrary files. Validate paths with path.resolve() and check they're within allowed directories.",
|
|
203
|
+
severity: "high"
|
|
110
204
|
},
|
|
111
205
|
{
|
|
112
206
|
id: "nosql-where",
|
|
113
207
|
name: "NoSQL Injection ($where)",
|
|
114
208
|
regex: /\.(find|findOne)\s*\(\s*\{[^}]*\$where\s*:/g,
|
|
115
|
-
fixPrompt: "$where executes JavaScript and enables NoSQL injection. Use standard MongoDB query operators."
|
|
209
|
+
fixPrompt: "$where executes JavaScript and enables NoSQL injection. Use standard MongoDB query operators.",
|
|
210
|
+
severity: "high"
|
|
116
211
|
},
|
|
117
212
|
{
|
|
118
213
|
id: "pickle-load",
|
|
119
214
|
name: "Insecure Pickle (Python)",
|
|
120
215
|
regex: /pickle\.loads?\s*\(\s*(?:request|user|input|data|file)/gi,
|
|
121
|
-
fixPrompt: "pickle.load with untrusted data allows arbitrary code execution. Use JSON for untrusted data."
|
|
216
|
+
fixPrompt: "pickle.load with untrusted data allows arbitrary code execution. Use JSON for untrusted data.",
|
|
217
|
+
severity: "critical"
|
|
122
218
|
},
|
|
123
219
|
{
|
|
124
220
|
id: "python-shell",
|
|
125
221
|
name: "Shell Injection (Python)",
|
|
126
222
|
regex: /subprocess\.\w+\s*\([^)]*shell\s*=\s*True[^)]*(?:request|user|input|param)/gi,
|
|
127
|
-
fixPrompt: "shell=True with user input enables command injection. Pass command as a list without shell=True."
|
|
223
|
+
fixPrompt: "shell=True with user input enables command injection. Pass command as a list without shell=True.",
|
|
224
|
+
severity: "critical"
|
|
128
225
|
}
|
|
129
226
|
];
|
|
130
227
|
|
|
@@ -186,6 +283,29 @@ function getLineNumber(content, matchIndex) {
|
|
|
186
283
|
`);
|
|
187
284
|
return lines.length;
|
|
188
285
|
}
|
|
286
|
+
function checkGitignore(dir) {
|
|
287
|
+
const warnings = [];
|
|
288
|
+
const gitignorePath = join(dir, ".gitignore");
|
|
289
|
+
const envPath = join(dir, ".env");
|
|
290
|
+
if (!existsSync(envPath)) {
|
|
291
|
+
return warnings;
|
|
292
|
+
}
|
|
293
|
+
if (!existsSync(gitignorePath)) {
|
|
294
|
+
warnings.push(".env file exists but no .gitignore found. Create a .gitignore and add .env to prevent committing secrets.");
|
|
295
|
+
return warnings;
|
|
296
|
+
}
|
|
297
|
+
try {
|
|
298
|
+
const gitignoreContent = readFileSync(gitignorePath, "utf-8");
|
|
299
|
+
const lines = gitignoreContent.split(`
|
|
300
|
+
`).map((l) => l.trim());
|
|
301
|
+
const envIgnored = lines.some((line) => line === ".env" || line === ".env*" || line === "*.env" || line === ".env.local" || line.startsWith(".env"));
|
|
302
|
+
if (!envIgnored) {
|
|
303
|
+
warnings.push(".env file exists but is not in .gitignore. Add .env to .gitignore to prevent committing secrets.");
|
|
304
|
+
}
|
|
305
|
+
} catch {
|
|
306
|
+
}
|
|
307
|
+
return warnings;
|
|
308
|
+
}
|
|
189
309
|
function scanFile(filePath) {
|
|
190
310
|
const issues = [];
|
|
191
311
|
let content;
|
|
@@ -206,7 +326,8 @@ function scanFile(filePath) {
|
|
|
206
326
|
patternId: pattern.id,
|
|
207
327
|
patternName: pattern.name,
|
|
208
328
|
fixPrompt: pattern.fixPrompt,
|
|
209
|
-
match: matchText.length > 60 ? matchText.substring(0, 60) + "..." : matchText
|
|
329
|
+
match: matchText.length > 60 ? matchText.substring(0, 60) + "..." : matchText,
|
|
330
|
+
severity: pattern.severity
|
|
210
331
|
});
|
|
211
332
|
}
|
|
212
333
|
}
|
|
@@ -215,15 +336,25 @@ function scanFile(filePath) {
|
|
|
215
336
|
function scanFiles(dir) {
|
|
216
337
|
const files = collectFiles(dir);
|
|
217
338
|
const allIssues = [];
|
|
339
|
+
const warnings = checkGitignore(dir);
|
|
218
340
|
for (const file of files) {
|
|
219
341
|
const issues = scanFile(file);
|
|
220
342
|
allIssues.push(...issues);
|
|
221
343
|
}
|
|
222
|
-
|
|
344
|
+
const severityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
345
|
+
allIssues.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
|
|
346
|
+
return { issues: allIssues, warnings };
|
|
223
347
|
}
|
|
224
348
|
|
|
225
349
|
// src/prompter.ts
|
|
226
|
-
|
|
350
|
+
var severityColors = {
|
|
351
|
+
critical: "\x1B[31m",
|
|
352
|
+
high: "\x1B[33m",
|
|
353
|
+
medium: "\x1B[36m",
|
|
354
|
+
low: "\x1B[37m"
|
|
355
|
+
};
|
|
356
|
+
var reset = "\x1B[0m";
|
|
357
|
+
function formatAgentPrompt(issues, useColors = true) {
|
|
227
358
|
if (!issues || issues.length === 0) {
|
|
228
359
|
return "";
|
|
229
360
|
}
|
|
@@ -234,7 +365,10 @@ function formatAgentPrompt(issues) {
|
|
|
234
365
|
""
|
|
235
366
|
];
|
|
236
367
|
issues.forEach((issue, index) => {
|
|
237
|
-
|
|
368
|
+
const severityLabel = issue.severity.toUpperCase();
|
|
369
|
+
const color = useColors ? severityColors[issue.severity] : "";
|
|
370
|
+
const colorReset = useColors ? reset : "";
|
|
371
|
+
lines.push(`${color}[${severityLabel}]${colorReset} [TASK ${index + 1}] Fix ${issue.patternName} in ${issue.file} at line ${issue.line}`);
|
|
238
372
|
lines.push(`[FOUND]: ${issue.match}`);
|
|
239
373
|
lines.push(`[INSTRUCTION]: ${issue.fixPrompt}`);
|
|
240
374
|
lines.push("");
|
|
@@ -255,9 +389,39 @@ function generateSummary(issues) {
|
|
|
255
389
|
}
|
|
256
390
|
return summary;
|
|
257
391
|
}
|
|
392
|
+
function generateSeveritySummary(issues) {
|
|
393
|
+
const summary = {
|
|
394
|
+
critical: 0,
|
|
395
|
+
high: 0,
|
|
396
|
+
medium: 0,
|
|
397
|
+
low: 0
|
|
398
|
+
};
|
|
399
|
+
for (const issue of issues) {
|
|
400
|
+
summary[issue.severity]++;
|
|
401
|
+
}
|
|
402
|
+
return summary;
|
|
403
|
+
}
|
|
404
|
+
function formatJson(issues, warnings) {
|
|
405
|
+
return JSON.stringify({
|
|
406
|
+
summary: {
|
|
407
|
+
total: issues.length,
|
|
408
|
+
bySeverity: generateSeveritySummary(issues),
|
|
409
|
+
byType: generateSummary(issues)
|
|
410
|
+
},
|
|
411
|
+
issues: issues.map((issue) => ({
|
|
412
|
+
severity: issue.severity,
|
|
413
|
+
type: issue.patternName,
|
|
414
|
+
file: issue.file,
|
|
415
|
+
line: issue.line,
|
|
416
|
+
match: issue.match,
|
|
417
|
+
fix: issue.fixPrompt
|
|
418
|
+
})),
|
|
419
|
+
warnings
|
|
420
|
+
}, null, 2);
|
|
421
|
+
}
|
|
258
422
|
|
|
259
423
|
// src/init.ts
|
|
260
|
-
import { writeFileSync, existsSync, readFileSync as readFileSync2 } from "fs";
|
|
424
|
+
import { writeFileSync, existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
261
425
|
import { join as join2 } from "path";
|
|
262
426
|
var CURSORRULES_CONTENT = `# Vibe Shield Security Rules
|
|
263
427
|
|
|
@@ -272,7 +436,7 @@ Before marking a task as done, run \`npx vibe-shield\`. If issues are found, fol
|
|
|
272
436
|
`;
|
|
273
437
|
function initVibeShield(dir) {
|
|
274
438
|
const cursorrulesPath = join2(dir, ".cursorrules");
|
|
275
|
-
if (
|
|
439
|
+
if (existsSync2(cursorrulesPath)) {
|
|
276
440
|
try {
|
|
277
441
|
const existingContent = readFileSync2(cursorrulesPath, "utf-8");
|
|
278
442
|
if (existingContent.includes("vibe-shield") || existingContent.includes("Vibe Shield")) {
|
|
@@ -316,7 +480,7 @@ function initVibeShield(dir) {
|
|
|
316
480
|
}
|
|
317
481
|
|
|
318
482
|
// src/cli.ts
|
|
319
|
-
var VERSION = "1.
|
|
483
|
+
var VERSION = "1.1.0";
|
|
320
484
|
var colors = {
|
|
321
485
|
reset: "\x1B[0m",
|
|
322
486
|
cyan: "\x1B[36m",
|
|
@@ -324,7 +488,8 @@ var colors = {
|
|
|
324
488
|
red: "\x1B[31m",
|
|
325
489
|
yellow: "\x1B[33m",
|
|
326
490
|
dim: "\x1B[2m",
|
|
327
|
-
bold: "\x1B[1m"
|
|
491
|
+
bold: "\x1B[1m",
|
|
492
|
+
magenta: "\x1B[35m"
|
|
328
493
|
};
|
|
329
494
|
function printBanner() {
|
|
330
495
|
console.log(colors.cyan + `
|
|
@@ -345,10 +510,14 @@ function printHelp() {
|
|
|
345
510
|
console.log(colors.green + " init" + colors.reset + colors.dim + " Create .cursorrules for AI agent integration" + colors.reset);
|
|
346
511
|
console.log(colors.green + " help" + colors.reset + colors.dim + " Show this help message" + colors.reset);
|
|
347
512
|
console.log(colors.green + " version" + colors.reset + colors.dim + ` Show version number
|
|
513
|
+
` + colors.reset);
|
|
514
|
+
console.log("Options:");
|
|
515
|
+
console.log(colors.green + " --json" + colors.reset + colors.dim + ` Output results as JSON
|
|
348
516
|
` + colors.reset);
|
|
349
517
|
console.log("Examples:");
|
|
350
518
|
console.log(colors.dim + " npx vibe-shield" + colors.reset);
|
|
351
519
|
console.log(colors.dim + " npx vibe-shield scan ./src" + colors.reset);
|
|
520
|
+
console.log(colors.dim + " npx vibe-shield scan . --json" + colors.reset);
|
|
352
521
|
console.log(colors.dim + ` npx vibe-shield init
|
|
353
522
|
` + colors.reset);
|
|
354
523
|
}
|
|
@@ -362,32 +531,60 @@ function runInit(dir) {
|
|
|
362
531
|
const result = initVibeShield(dir);
|
|
363
532
|
if (result.success) {
|
|
364
533
|
console.log(colors.green + "✓ " + colors.reset + result.message);
|
|
365
|
-
console.log(colors.dim + `
|
|
534
|
+
console.log(colors.dim + ` Path: ${result.path}
|
|
366
535
|
` + colors.reset);
|
|
367
536
|
console.log("Your AI agent will now run vibe-shield before completing tasks.");
|
|
368
537
|
console.log(colors.dim + `Happy vibe coding!
|
|
369
538
|
` + colors.reset);
|
|
370
539
|
process.exit(0);
|
|
371
540
|
} else {
|
|
372
|
-
console.log(colors.
|
|
373
|
-
process.exit(
|
|
541
|
+
console.log(colors.yellow + "! " + colors.reset + result.message);
|
|
542
|
+
process.exit(0);
|
|
374
543
|
}
|
|
375
544
|
}
|
|
376
|
-
function runScan(dir) {
|
|
377
|
-
|
|
378
|
-
|
|
545
|
+
function runScan(dir, jsonOutput) {
|
|
546
|
+
if (!jsonOutput) {
|
|
547
|
+
printBanner();
|
|
548
|
+
console.log(colors.yellow + `Scanning ${dir}...
|
|
379
549
|
` + colors.reset);
|
|
380
|
-
|
|
550
|
+
}
|
|
551
|
+
const { issues, warnings } = scanFiles(dir);
|
|
552
|
+
if (jsonOutput) {
|
|
553
|
+
console.log(formatJson(issues, warnings));
|
|
554
|
+
process.exit(issues.length > 0 ? 1 : 0);
|
|
555
|
+
}
|
|
556
|
+
if (warnings.length > 0) {
|
|
557
|
+
console.log(colors.yellow + "Warnings:" + colors.reset);
|
|
558
|
+
for (const warning of warnings) {
|
|
559
|
+
console.log(colors.yellow + " ⚠ " + colors.reset + warning);
|
|
560
|
+
}
|
|
561
|
+
console.log("");
|
|
562
|
+
}
|
|
381
563
|
if (issues.length === 0) {
|
|
382
564
|
console.log(colors.green + colors.bold + "✓ SAFE" + colors.reset);
|
|
383
565
|
console.log(colors.dim + `No security issues detected. Ship it!
|
|
384
566
|
` + colors.reset);
|
|
385
567
|
process.exit(0);
|
|
386
568
|
}
|
|
387
|
-
const
|
|
569
|
+
const severitySummary = generateSeveritySummary(issues);
|
|
388
570
|
console.log(colors.red + colors.bold + `✗ Found ${issues.length} security issue${issues.length > 1 ? "s" : ""}
|
|
389
571
|
` + colors.reset);
|
|
390
|
-
console.log("
|
|
572
|
+
console.log("By severity:");
|
|
573
|
+
if (severitySummary.critical > 0) {
|
|
574
|
+
console.log(colors.red + ` • Critical: ${severitySummary.critical}` + colors.reset);
|
|
575
|
+
}
|
|
576
|
+
if (severitySummary.high > 0) {
|
|
577
|
+
console.log(colors.yellow + ` • High: ${severitySummary.high}` + colors.reset);
|
|
578
|
+
}
|
|
579
|
+
if (severitySummary.medium > 0) {
|
|
580
|
+
console.log(colors.cyan + ` • Medium: ${severitySummary.medium}` + colors.reset);
|
|
581
|
+
}
|
|
582
|
+
if (severitySummary.low > 0) {
|
|
583
|
+
console.log(colors.dim + ` • Low: ${severitySummary.low}` + colors.reset);
|
|
584
|
+
}
|
|
585
|
+
console.log("");
|
|
586
|
+
const summary = generateSummary(issues);
|
|
587
|
+
console.log("By type:");
|
|
391
588
|
for (const [pattern, count] of Object.entries(summary)) {
|
|
392
589
|
console.log(colors.dim + ` • ${pattern}: ${count}` + colors.reset);
|
|
393
590
|
}
|
|
@@ -398,14 +595,16 @@ function runScan(dir) {
|
|
|
398
595
|
process.exit(1);
|
|
399
596
|
}
|
|
400
597
|
var args = process.argv.slice(2);
|
|
401
|
-
var
|
|
402
|
-
var
|
|
598
|
+
var jsonFlag = args.includes("--json");
|
|
599
|
+
var filteredArgs = args.filter((arg) => !arg.startsWith("--"));
|
|
600
|
+
var command = filteredArgs[0] || "scan";
|
|
601
|
+
var targetDir = filteredArgs[1] || process.cwd();
|
|
403
602
|
switch (command) {
|
|
404
603
|
case "init":
|
|
405
604
|
runInit(targetDir === process.cwd() ? process.cwd() : targetDir);
|
|
406
605
|
break;
|
|
407
606
|
case "scan":
|
|
408
|
-
runScan(targetDir);
|
|
607
|
+
runScan(targetDir, jsonFlag);
|
|
409
608
|
break;
|
|
410
609
|
case "help":
|
|
411
610
|
case "--help":
|
|
@@ -421,7 +620,7 @@ switch (command) {
|
|
|
421
620
|
break;
|
|
422
621
|
default:
|
|
423
622
|
if (command.startsWith(".") || command.startsWith("/")) {
|
|
424
|
-
runScan(command);
|
|
623
|
+
runScan(command, jsonFlag);
|
|
425
624
|
} else {
|
|
426
625
|
console.log(colors.red + `Unknown command: ${command}` + colors.reset);
|
|
427
626
|
console.log(colors.dim + `Run "vibe-shield help" for usage.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { scanFiles } from "./scanner";
|
|
2
|
-
export { formatAgentPrompt, generateSummary } from "./prompter";
|
|
2
|
+
export { formatAgentPrompt, generateSummary, generateSeveritySummary, formatJson, } from "./prompter";
|
|
3
3
|
export { initVibeShield } from "./init";
|
|
4
4
|
export { securityPatterns } from "./patterns";
|
|
5
|
-
export type { SecurityPattern, SecurityIssue, InitResult, IssueSummary } from "./types";
|
|
5
|
+
export type { SecurityPattern, SecurityIssue, InitResult, IssueSummary, ScanResult, Severity, } from "./types";
|
package/dist/index.js
CHANGED
|
@@ -1,128 +1,225 @@
|
|
|
1
1
|
// src/scanner.ts
|
|
2
|
-
import { readFileSync, readdirSync, statSync } from "fs";
|
|
2
|
+
import { readFileSync, readdirSync, statSync, existsSync } from "fs";
|
|
3
3
|
import { join, extname } from "path";
|
|
4
4
|
|
|
5
5
|
// src/patterns.ts
|
|
6
6
|
var securityPatterns = [
|
|
7
|
+
{
|
|
8
|
+
id: "aws-access-key",
|
|
9
|
+
name: "AWS Access Key",
|
|
10
|
+
regex: /['"`](AKIA[0-9A-Z]{16})['"`]/g,
|
|
11
|
+
fixPrompt: "AWS access key detected. Remove immediately and rotate in AWS console. Use environment variables or IAM roles.",
|
|
12
|
+
severity: "critical"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
id: "aws-secret-key",
|
|
16
|
+
name: "AWS Secret Key",
|
|
17
|
+
regex: /aws_?secret_?access_?key\s*[:=]\s*['"`][A-Za-z0-9\/+=]{40}['"`]/gi,
|
|
18
|
+
fixPrompt: "AWS secret key detected. Remove immediately and rotate in AWS console. Use environment variables.",
|
|
19
|
+
severity: "critical"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
id: "openai-key",
|
|
23
|
+
name: "OpenAI API Key",
|
|
24
|
+
regex: /['"`](sk-[A-Za-z0-9]{48,})['"`]/g,
|
|
25
|
+
fixPrompt: "OpenAI API key detected. Remove and rotate at platform.openai.com. Use process.env.OPENAI_API_KEY.",
|
|
26
|
+
severity: "critical"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: "anthropic-key",
|
|
30
|
+
name: "Anthropic API Key",
|
|
31
|
+
regex: /['"`](sk-ant-[A-Za-z0-9\-]{80,})['"`]/g,
|
|
32
|
+
fixPrompt: "Anthropic API key detected. Remove and rotate at console.anthropic.com. Use process.env.ANTHROPIC_API_KEY.",
|
|
33
|
+
severity: "critical"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: "stripe-secret",
|
|
37
|
+
name: "Stripe Secret Key",
|
|
38
|
+
regex: /['"`](sk_live_[A-Za-z0-9]{24,})['"`]/g,
|
|
39
|
+
fixPrompt: "Stripe live secret key detected. Remove and rotate in Stripe dashboard. Use process.env.STRIPE_SECRET_KEY.",
|
|
40
|
+
severity: "critical"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: "stripe-restricted",
|
|
44
|
+
name: "Stripe Restricted Key",
|
|
45
|
+
regex: /['"`](rk_live_[A-Za-z0-9]{24,})['"`]/g,
|
|
46
|
+
fixPrompt: "Stripe restricted key detected. Remove and rotate in Stripe dashboard. Use environment variables.",
|
|
47
|
+
severity: "critical"
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: "github-token",
|
|
51
|
+
name: "GitHub Token",
|
|
52
|
+
regex: /['"`](ghp_[A-Za-z0-9]{36}|github_pat_[A-Za-z0-9_]{22,})['"`]/g,
|
|
53
|
+
fixPrompt: "GitHub token detected. Remove and rotate at github.com/settings/tokens. Use process.env.GITHUB_TOKEN.",
|
|
54
|
+
severity: "critical"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: "slack-token",
|
|
58
|
+
name: "Slack Token",
|
|
59
|
+
regex: /['"`](xox[baprs]-[A-Za-z0-9\-]{10,})['"`]/g,
|
|
60
|
+
fixPrompt: "Slack token detected. Remove and rotate in Slack app settings. Use environment variables.",
|
|
61
|
+
severity: "critical"
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: "twilio-key",
|
|
65
|
+
name: "Twilio API Key",
|
|
66
|
+
regex: /['"`](SK[A-Za-z0-9]{32})['"`]/g,
|
|
67
|
+
fixPrompt: "Twilio API key detected. Remove and rotate in Twilio console. Use environment variables.",
|
|
68
|
+
severity: "critical"
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: "sendgrid-key",
|
|
72
|
+
name: "SendGrid API Key",
|
|
73
|
+
regex: /['"`](SG\.[A-Za-z0-9\-_]{22}\.[A-Za-z0-9\-_]{43})['"`]/g,
|
|
74
|
+
fixPrompt: "SendGrid API key detected. Remove and rotate in SendGrid dashboard. Use environment variables.",
|
|
75
|
+
severity: "critical"
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: "private-key",
|
|
79
|
+
name: "Private Key",
|
|
80
|
+
regex: /-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/g,
|
|
81
|
+
fixPrompt: "Private key detected in code. Move to a secure file outside repo or use a secrets manager.",
|
|
82
|
+
severity: "critical"
|
|
83
|
+
},
|
|
7
84
|
{
|
|
8
85
|
id: "hardcoded-secret",
|
|
9
86
|
name: "Hardcoded Secret",
|
|
10
87
|
regex: /(?:api_?key|api_?secret|secret_?key|auth_?token|access_?token|private_?key|client_?secret)\s*[:=]\s*['"`][A-Za-z0-9_\-\.\/\+]{8,}['"`]/gi,
|
|
11
|
-
fixPrompt: "Move this secret to an environment variable. Add
|
|
88
|
+
fixPrompt: "Move this secret to an environment variable. Add to .env and use process.env.YOUR_SECRET. Add .env to .gitignore.",
|
|
89
|
+
severity: "high"
|
|
12
90
|
},
|
|
13
91
|
{
|
|
14
92
|
id: "hardcoded-password",
|
|
15
93
|
name: "Hardcoded Password",
|
|
16
94
|
regex: /(?:password|passwd|pwd)\s*[:=]\s*['"`][^'"`\s]{4,}['"`]/gi,
|
|
17
|
-
fixPrompt: "Move this password to an environment variable. Never commit passwords to version control."
|
|
18
|
-
|
|
19
|
-
{
|
|
20
|
-
id: "aws-key",
|
|
21
|
-
name: "AWS Access Key",
|
|
22
|
-
regex: /['"`](AKIA[0-9A-Z]{16})['"`]/g,
|
|
23
|
-
fixPrompt: "AWS access key detected. Remove it immediately and rotate the key in AWS console. Use environment variables or AWS IAM roles."
|
|
95
|
+
fixPrompt: "Move this password to an environment variable. Never commit passwords to version control.",
|
|
96
|
+
severity: "high"
|
|
24
97
|
},
|
|
25
98
|
{
|
|
26
99
|
id: "jwt-secret-inline",
|
|
27
100
|
name: "Hardcoded JWT Secret",
|
|
28
101
|
regex: /jwt\.sign\s*\([^)]+,\s*['"`][^'"`]{8,}['"`]/gi,
|
|
29
|
-
fixPrompt: "Move JWT secret to an environment variable. Hardcoded secrets get committed to version control."
|
|
102
|
+
fixPrompt: "Move JWT secret to an environment variable. Hardcoded secrets get committed to version control.",
|
|
103
|
+
severity: "high"
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: "database-url",
|
|
107
|
+
name: "Database Connection String",
|
|
108
|
+
regex: /['"`](mongodb(\+srv)?|postgres(ql)?|mysql|redis):\/\/[^'"`\s]{10,}['"`]/gi,
|
|
109
|
+
fixPrompt: "Database connection string with credentials detected. Use process.env.DATABASE_URL.",
|
|
110
|
+
severity: "high"
|
|
30
111
|
},
|
|
31
112
|
{
|
|
32
113
|
id: "sql-injection-template",
|
|
33
114
|
name: "SQL Injection",
|
|
34
115
|
regex: /(?:query|execute)\s*\(\s*`(?:SELECT|INSERT|UPDATE|DELETE|DROP)[^`]*\$\{/gi,
|
|
35
|
-
fixPrompt: "Use parameterized queries instead of template literals. Example: query('SELECT * FROM users WHERE id = ?', [userId])"
|
|
116
|
+
fixPrompt: "Use parameterized queries instead of template literals. Example: query('SELECT * FROM users WHERE id = ?', [userId])",
|
|
117
|
+
severity: "high"
|
|
36
118
|
},
|
|
37
119
|
{
|
|
38
120
|
id: "sql-injection-concat",
|
|
39
121
|
name: "SQL Injection",
|
|
40
122
|
regex: /(?:query|execute)\s*\(\s*['"](?:SELECT|INSERT|UPDATE|DELETE)[^'"]*['"]\s*\+/gi,
|
|
41
|
-
fixPrompt: "Never concatenate variables into SQL strings. Use parameterized queries with placeholders."
|
|
123
|
+
fixPrompt: "Never concatenate variables into SQL strings. Use parameterized queries with placeholders.",
|
|
124
|
+
severity: "high"
|
|
42
125
|
},
|
|
43
126
|
{
|
|
44
127
|
id: "command-injection",
|
|
45
128
|
name: "Command Injection",
|
|
46
129
|
regex: /(?:exec|execSync)\s*\(\s*`[^`]*\$\{/g,
|
|
47
|
-
fixPrompt: "Avoid template literals in shell commands. Use spawn() with an array of arguments
|
|
130
|
+
fixPrompt: "Avoid template literals in shell commands. Use spawn() with an array of arguments.",
|
|
131
|
+
severity: "critical"
|
|
48
132
|
},
|
|
49
133
|
{
|
|
50
134
|
id: "command-injection-concat",
|
|
51
135
|
name: "Command Injection",
|
|
52
136
|
regex: /(?:exec|execSync)\s*\([^)]*\+\s*(?:req\.|user|input|param|query|body)/gi,
|
|
53
|
-
fixPrompt: "User input in shell commands allows arbitrary command execution. Use spawn() with argument arrays."
|
|
137
|
+
fixPrompt: "User input in shell commands allows arbitrary command execution. Use spawn() with argument arrays.",
|
|
138
|
+
severity: "critical"
|
|
54
139
|
},
|
|
55
140
|
{
|
|
56
141
|
id: "eval-usage",
|
|
57
142
|
name: "Dangerous eval()",
|
|
58
143
|
regex: /[=:]\s*eval\s*\(\s*(?:req\.|user|input|param|query|body|data)/gi,
|
|
59
|
-
fixPrompt: "eval() with user input allows arbitrary code execution. Use JSON.parse() for JSON, or refactor to avoid eval
|
|
144
|
+
fixPrompt: "eval() with user input allows arbitrary code execution. Use JSON.parse() for JSON, or refactor to avoid eval.",
|
|
145
|
+
severity: "high"
|
|
60
146
|
},
|
|
61
147
|
{
|
|
62
148
|
id: "new-function",
|
|
63
149
|
name: "Dangerous Function Constructor",
|
|
64
150
|
regex: /new\s+Function\s*\([^)]*(?:req\.|user|input|param|query|body)/gi,
|
|
65
|
-
fixPrompt: "new Function() with user input is as dangerous as eval(). Refactor to avoid dynamic code generation."
|
|
151
|
+
fixPrompt: "new Function() with user input is as dangerous as eval(). Refactor to avoid dynamic code generation.",
|
|
152
|
+
severity: "high"
|
|
66
153
|
},
|
|
67
154
|
{
|
|
68
155
|
id: "innerhtml-variable",
|
|
69
156
|
name: "XSS via innerHTML",
|
|
70
157
|
regex: /\.innerHTML\s*=\s*(?:req\.|user|input|param|query|body|data|props\.|this\.)/gi,
|
|
71
|
-
fixPrompt: "Setting innerHTML with user data enables XSS attacks. Use textContent or sanitize with DOMPurify."
|
|
158
|
+
fixPrompt: "Setting innerHTML with user data enables XSS attacks. Use textContent or sanitize with DOMPurify.",
|
|
159
|
+
severity: "high"
|
|
72
160
|
},
|
|
73
161
|
{
|
|
74
162
|
id: "react-dangerous-html",
|
|
75
163
|
name: "React XSS Risk",
|
|
76
164
|
regex: /dangerouslySetInnerHTML\s*=\s*\{\s*\{\s*__html\s*:\s*(?:props\.|this\.|data|user|input)/gi,
|
|
77
|
-
fixPrompt: "dangerouslySetInnerHTML with user data enables XSS. Sanitize HTML with DOMPurify first."
|
|
165
|
+
fixPrompt: "dangerouslySetInnerHTML with user data enables XSS. Sanitize HTML with DOMPurify first.",
|
|
166
|
+
severity: "high"
|
|
78
167
|
},
|
|
79
168
|
{
|
|
80
169
|
id: "weak-hash-md5",
|
|
81
170
|
name: "Weak Hash (MD5)",
|
|
82
171
|
regex: /createHash\s*\(\s*['"`]md5['"`]\s*\)/g,
|
|
83
|
-
fixPrompt: "MD5 is broken. Use crypto.createHash('sha256'). For passwords, use bcrypt or argon2."
|
|
172
|
+
fixPrompt: "MD5 is broken. Use crypto.createHash('sha256'). For passwords, use bcrypt or argon2.",
|
|
173
|
+
severity: "medium"
|
|
84
174
|
},
|
|
85
175
|
{
|
|
86
176
|
id: "weak-hash-sha1",
|
|
87
177
|
name: "Weak Hash (SHA1)",
|
|
88
178
|
regex: /createHash\s*\(\s*['"`]sha1['"`]\s*\)/g,
|
|
89
|
-
fixPrompt: "SHA1 is deprecated for security. Use crypto.createHash('sha256'). For passwords, use bcrypt or argon2."
|
|
179
|
+
fixPrompt: "SHA1 is deprecated for security. Use crypto.createHash('sha256'). For passwords, use bcrypt or argon2.",
|
|
180
|
+
severity: "medium"
|
|
90
181
|
},
|
|
91
182
|
{
|
|
92
183
|
id: "ssl-disabled",
|
|
93
184
|
name: "SSL Verification Disabled",
|
|
94
185
|
regex: /rejectUnauthorized\s*:\s*false/g,
|
|
95
|
-
fixPrompt: "Disabling SSL verification allows man-in-the-middle attacks. Remove this or set to true."
|
|
186
|
+
fixPrompt: "Disabling SSL verification allows man-in-the-middle attacks. Remove this or set to true.",
|
|
187
|
+
severity: "medium"
|
|
96
188
|
},
|
|
97
189
|
{
|
|
98
190
|
id: "cors-wildcard",
|
|
99
191
|
name: "CORS Allows All Origins",
|
|
100
192
|
regex: /['"`]Access-Control-Allow-Origin['"`]\s*[,:]\s*['"`]\*['"`]/g,
|
|
101
|
-
fixPrompt: "CORS wildcard (*) allows any website to make requests. Specify allowed origins explicitly."
|
|
193
|
+
fixPrompt: "CORS wildcard (*) allows any website to make requests. Specify allowed origins explicitly.",
|
|
194
|
+
severity: "medium"
|
|
102
195
|
},
|
|
103
196
|
{
|
|
104
197
|
id: "path-traversal",
|
|
105
198
|
name: "Path Traversal Risk",
|
|
106
199
|
regex: /(?:readFile|writeFile|readFileSync|writeFileSync)\s*\(\s*(?:req\.|user|input|param|query|body)/gi,
|
|
107
|
-
fixPrompt: "User input in file paths allows reading/writing arbitrary files. Validate paths with path.resolve() and check they're within allowed directories."
|
|
200
|
+
fixPrompt: "User input in file paths allows reading/writing arbitrary files. Validate paths with path.resolve() and check they're within allowed directories.",
|
|
201
|
+
severity: "high"
|
|
108
202
|
},
|
|
109
203
|
{
|
|
110
204
|
id: "nosql-where",
|
|
111
205
|
name: "NoSQL Injection ($where)",
|
|
112
206
|
regex: /\.(find|findOne)\s*\(\s*\{[^}]*\$where\s*:/g,
|
|
113
|
-
fixPrompt: "$where executes JavaScript and enables NoSQL injection. Use standard MongoDB query operators."
|
|
207
|
+
fixPrompt: "$where executes JavaScript and enables NoSQL injection. Use standard MongoDB query operators.",
|
|
208
|
+
severity: "high"
|
|
114
209
|
},
|
|
115
210
|
{
|
|
116
211
|
id: "pickle-load",
|
|
117
212
|
name: "Insecure Pickle (Python)",
|
|
118
213
|
regex: /pickle\.loads?\s*\(\s*(?:request|user|input|data|file)/gi,
|
|
119
|
-
fixPrompt: "pickle.load with untrusted data allows arbitrary code execution. Use JSON for untrusted data."
|
|
214
|
+
fixPrompt: "pickle.load with untrusted data allows arbitrary code execution. Use JSON for untrusted data.",
|
|
215
|
+
severity: "critical"
|
|
120
216
|
},
|
|
121
217
|
{
|
|
122
218
|
id: "python-shell",
|
|
123
219
|
name: "Shell Injection (Python)",
|
|
124
220
|
regex: /subprocess\.\w+\s*\([^)]*shell\s*=\s*True[^)]*(?:request|user|input|param)/gi,
|
|
125
|
-
fixPrompt: "shell=True with user input enables command injection. Pass command as a list without shell=True."
|
|
221
|
+
fixPrompt: "shell=True with user input enables command injection. Pass command as a list without shell=True.",
|
|
222
|
+
severity: "critical"
|
|
126
223
|
}
|
|
127
224
|
];
|
|
128
225
|
|
|
@@ -184,6 +281,29 @@ function getLineNumber(content, matchIndex) {
|
|
|
184
281
|
`);
|
|
185
282
|
return lines.length;
|
|
186
283
|
}
|
|
284
|
+
function checkGitignore(dir) {
|
|
285
|
+
const warnings = [];
|
|
286
|
+
const gitignorePath = join(dir, ".gitignore");
|
|
287
|
+
const envPath = join(dir, ".env");
|
|
288
|
+
if (!existsSync(envPath)) {
|
|
289
|
+
return warnings;
|
|
290
|
+
}
|
|
291
|
+
if (!existsSync(gitignorePath)) {
|
|
292
|
+
warnings.push(".env file exists but no .gitignore found. Create a .gitignore and add .env to prevent committing secrets.");
|
|
293
|
+
return warnings;
|
|
294
|
+
}
|
|
295
|
+
try {
|
|
296
|
+
const gitignoreContent = readFileSync(gitignorePath, "utf-8");
|
|
297
|
+
const lines = gitignoreContent.split(`
|
|
298
|
+
`).map((l) => l.trim());
|
|
299
|
+
const envIgnored = lines.some((line) => line === ".env" || line === ".env*" || line === "*.env" || line === ".env.local" || line.startsWith(".env"));
|
|
300
|
+
if (!envIgnored) {
|
|
301
|
+
warnings.push(".env file exists but is not in .gitignore. Add .env to .gitignore to prevent committing secrets.");
|
|
302
|
+
}
|
|
303
|
+
} catch {
|
|
304
|
+
}
|
|
305
|
+
return warnings;
|
|
306
|
+
}
|
|
187
307
|
function scanFile(filePath) {
|
|
188
308
|
const issues = [];
|
|
189
309
|
let content;
|
|
@@ -204,7 +324,8 @@ function scanFile(filePath) {
|
|
|
204
324
|
patternId: pattern.id,
|
|
205
325
|
patternName: pattern.name,
|
|
206
326
|
fixPrompt: pattern.fixPrompt,
|
|
207
|
-
match: matchText.length > 60 ? matchText.substring(0, 60) + "..." : matchText
|
|
327
|
+
match: matchText.length > 60 ? matchText.substring(0, 60) + "..." : matchText,
|
|
328
|
+
severity: pattern.severity
|
|
208
329
|
});
|
|
209
330
|
}
|
|
210
331
|
}
|
|
@@ -213,14 +334,24 @@ function scanFile(filePath) {
|
|
|
213
334
|
function scanFiles(dir) {
|
|
214
335
|
const files = collectFiles(dir);
|
|
215
336
|
const allIssues = [];
|
|
337
|
+
const warnings = checkGitignore(dir);
|
|
216
338
|
for (const file of files) {
|
|
217
339
|
const issues = scanFile(file);
|
|
218
340
|
allIssues.push(...issues);
|
|
219
341
|
}
|
|
220
|
-
|
|
342
|
+
const severityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
343
|
+
allIssues.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
|
|
344
|
+
return { issues: allIssues, warnings };
|
|
221
345
|
}
|
|
222
346
|
// src/prompter.ts
|
|
223
|
-
|
|
347
|
+
var severityColors = {
|
|
348
|
+
critical: "\x1B[31m",
|
|
349
|
+
high: "\x1B[33m",
|
|
350
|
+
medium: "\x1B[36m",
|
|
351
|
+
low: "\x1B[37m"
|
|
352
|
+
};
|
|
353
|
+
var reset = "\x1B[0m";
|
|
354
|
+
function formatAgentPrompt(issues, useColors = true) {
|
|
224
355
|
if (!issues || issues.length === 0) {
|
|
225
356
|
return "";
|
|
226
357
|
}
|
|
@@ -231,7 +362,10 @@ function formatAgentPrompt(issues) {
|
|
|
231
362
|
""
|
|
232
363
|
];
|
|
233
364
|
issues.forEach((issue, index) => {
|
|
234
|
-
|
|
365
|
+
const severityLabel = issue.severity.toUpperCase();
|
|
366
|
+
const color = useColors ? severityColors[issue.severity] : "";
|
|
367
|
+
const colorReset = useColors ? reset : "";
|
|
368
|
+
lines.push(`${color}[${severityLabel}]${colorReset} [TASK ${index + 1}] Fix ${issue.patternName} in ${issue.file} at line ${issue.line}`);
|
|
235
369
|
lines.push(`[FOUND]: ${issue.match}`);
|
|
236
370
|
lines.push(`[INSTRUCTION]: ${issue.fixPrompt}`);
|
|
237
371
|
lines.push("");
|
|
@@ -252,8 +386,38 @@ function generateSummary(issues) {
|
|
|
252
386
|
}
|
|
253
387
|
return summary;
|
|
254
388
|
}
|
|
389
|
+
function generateSeveritySummary(issues) {
|
|
390
|
+
const summary = {
|
|
391
|
+
critical: 0,
|
|
392
|
+
high: 0,
|
|
393
|
+
medium: 0,
|
|
394
|
+
low: 0
|
|
395
|
+
};
|
|
396
|
+
for (const issue of issues) {
|
|
397
|
+
summary[issue.severity]++;
|
|
398
|
+
}
|
|
399
|
+
return summary;
|
|
400
|
+
}
|
|
401
|
+
function formatJson(issues, warnings) {
|
|
402
|
+
return JSON.stringify({
|
|
403
|
+
summary: {
|
|
404
|
+
total: issues.length,
|
|
405
|
+
bySeverity: generateSeveritySummary(issues),
|
|
406
|
+
byType: generateSummary(issues)
|
|
407
|
+
},
|
|
408
|
+
issues: issues.map((issue) => ({
|
|
409
|
+
severity: issue.severity,
|
|
410
|
+
type: issue.patternName,
|
|
411
|
+
file: issue.file,
|
|
412
|
+
line: issue.line,
|
|
413
|
+
match: issue.match,
|
|
414
|
+
fix: issue.fixPrompt
|
|
415
|
+
})),
|
|
416
|
+
warnings
|
|
417
|
+
}, null, 2);
|
|
418
|
+
}
|
|
255
419
|
// src/init.ts
|
|
256
|
-
import { writeFileSync, existsSync, readFileSync as readFileSync2 } from "fs";
|
|
420
|
+
import { writeFileSync, existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
257
421
|
import { join as join2 } from "path";
|
|
258
422
|
var CURSORRULES_CONTENT = `# Vibe Shield Security Rules
|
|
259
423
|
|
|
@@ -268,7 +432,7 @@ Before marking a task as done, run \`npx vibe-shield\`. If issues are found, fol
|
|
|
268
432
|
`;
|
|
269
433
|
function initVibeShield(dir) {
|
|
270
434
|
const cursorrulesPath = join2(dir, ".cursorrules");
|
|
271
|
-
if (
|
|
435
|
+
if (existsSync2(cursorrulesPath)) {
|
|
272
436
|
try {
|
|
273
437
|
const existingContent = readFileSync2(cursorrulesPath, "utf-8");
|
|
274
438
|
if (existingContent.includes("vibe-shield") || existingContent.includes("Vibe Shield")) {
|
|
@@ -315,5 +479,7 @@ export {
|
|
|
315
479
|
scanFiles,
|
|
316
480
|
initVibeShield,
|
|
317
481
|
generateSummary,
|
|
482
|
+
generateSeveritySummary,
|
|
483
|
+
formatJson,
|
|
318
484
|
formatAgentPrompt
|
|
319
485
|
};
|
package/dist/prompter.d.ts
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
|
-
import type { SecurityIssue, IssueSummary } from "./types";
|
|
1
|
+
import type { SecurityIssue, IssueSummary, Severity } from "./types";
|
|
2
2
|
/**
|
|
3
3
|
* Format issues into an "Agent Protocol" string that AI agents can read and act on.
|
|
4
4
|
*/
|
|
5
|
-
export declare function formatAgentPrompt(issues: SecurityIssue[]): string;
|
|
5
|
+
export declare function formatAgentPrompt(issues: SecurityIssue[], useColors?: boolean): string;
|
|
6
6
|
/**
|
|
7
7
|
* Generate a summary of issues by type.
|
|
8
8
|
*/
|
|
9
9
|
export declare function generateSummary(issues: SecurityIssue[]): IssueSummary;
|
|
10
|
+
/**
|
|
11
|
+
* Generate severity summary
|
|
12
|
+
*/
|
|
13
|
+
export declare function generateSeveritySummary(issues: SecurityIssue[]): Record<Severity, number>;
|
|
14
|
+
/**
|
|
15
|
+
* Format issues as JSON for CI/CD pipelines
|
|
16
|
+
*/
|
|
17
|
+
export declare function formatJson(issues: SecurityIssue[], warnings: string[]): string;
|
package/dist/scanner.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ScanResult } from "./types";
|
|
2
2
|
/**
|
|
3
3
|
* Scan all files in a directory for security issues.
|
|
4
4
|
*/
|
|
5
|
-
export declare function scanFiles(dir: string):
|
|
5
|
+
export declare function scanFiles(dir: string): ScanResult;
|
package/dist/types.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
export type Severity = "critical" | "high" | "medium" | "low";
|
|
1
2
|
export interface SecurityPattern {
|
|
2
3
|
id: string;
|
|
3
4
|
name: string;
|
|
4
5
|
regex: RegExp;
|
|
5
6
|
fixPrompt: string;
|
|
7
|
+
severity: Severity;
|
|
6
8
|
}
|
|
7
9
|
export interface SecurityIssue {
|
|
8
10
|
file: string;
|
|
@@ -11,6 +13,7 @@ export interface SecurityIssue {
|
|
|
11
13
|
patternName: string;
|
|
12
14
|
fixPrompt: string;
|
|
13
15
|
match: string;
|
|
16
|
+
severity: Severity;
|
|
14
17
|
}
|
|
15
18
|
export interface InitResult {
|
|
16
19
|
success: boolean;
|
|
@@ -20,3 +23,7 @@ export interface InitResult {
|
|
|
20
23
|
export interface IssueSummary {
|
|
21
24
|
[patternName: string]: number;
|
|
22
25
|
}
|
|
26
|
+
export interface ScanResult {
|
|
27
|
+
issues: SecurityIssue[];
|
|
28
|
+
warnings: string[];
|
|
29
|
+
}
|