vibe-shield 1.0.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/LICENSE +21 -0
- package/README.md +51 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +431 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +319 -0
- package/dist/init.d.ts +5 -0
- package/dist/patterns.d.ts +6 -0
- package/dist/prompter.d.ts +9 -0
- package/dist/scanner.d.ts +5 -0
- package/dist/types.d.ts +22 -0
- package/package.json +51 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# vibe-shield
|
|
2
|
+
|
|
3
|
+
The security layer for Vibe Coding. A single command that scans your repo and generates auto-fix prompts for Cursor and Claude Code to secure your app instantly.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx vibe-shield
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
That's it. Run it in your project directory and it will scan for security issues.
|
|
12
|
+
|
|
13
|
+
## What it detects
|
|
14
|
+
|
|
15
|
+
- Hardcoded secrets (API keys, passwords, AWS keys, JWT secrets)
|
|
16
|
+
- SQL injection (template literals and string concatenation in queries)
|
|
17
|
+
- Command injection (user input in shell commands)
|
|
18
|
+
- XSS vulnerabilities (innerHTML with user data)
|
|
19
|
+
- Weak cryptography (MD5, SHA1)
|
|
20
|
+
- Security misconfigurations (disabled SSL, CORS wildcards)
|
|
21
|
+
- Path traversal (user input in file operations)
|
|
22
|
+
- NoSQL injection ($where operator)
|
|
23
|
+
- Python issues (pickle with untrusted data, shell=True)
|
|
24
|
+
|
|
25
|
+
## How it works
|
|
26
|
+
|
|
27
|
+
When vibe-shield finds an issue, it outputs a prompt your AI agent can follow:
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
[TASK 1] Fix Hardcoded Secret in src/db.ts at line 12
|
|
31
|
+
[FOUND]: api_key = "sk-1234..."
|
|
32
|
+
[INSTRUCTION]: Move this secret to an environment variable...
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Paste this output to Cursor or Claude Code and it will fix the issues for you.
|
|
36
|
+
|
|
37
|
+
## Agent integration
|
|
38
|
+
|
|
39
|
+
Run `npx vibe-shield init` to create a `.cursorrules` file that reminds your AI agent to run security checks before completing tasks.
|
|
40
|
+
|
|
41
|
+
## Development
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
bun install
|
|
45
|
+
bun run dev
|
|
46
|
+
bun run build
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## License
|
|
50
|
+
|
|
51
|
+
MIT
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/scanner.ts
|
|
4
|
+
import { readFileSync, readdirSync, statSync } from "fs";
|
|
5
|
+
import { join, extname } from "path";
|
|
6
|
+
|
|
7
|
+
// src/patterns.ts
|
|
8
|
+
var securityPatterns = [
|
|
9
|
+
{
|
|
10
|
+
id: "hardcoded-secret",
|
|
11
|
+
name: "Hardcoded Secret",
|
|
12
|
+
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 it to .env and use process.env.YOUR_SECRET. Add .env to .gitignore."
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
id: "hardcoded-password",
|
|
17
|
+
name: "Hardcoded Password",
|
|
18
|
+
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."
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: "jwt-secret-inline",
|
|
29
|
+
name: "Hardcoded JWT Secret",
|
|
30
|
+
regex: /jwt\.sign\s*\([^)]+,\s*['"`][^'"`]{8,}['"`]/gi,
|
|
31
|
+
fixPrompt: "Move JWT secret to an environment variable. Hardcoded secrets get committed to version control."
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: "sql-injection-template",
|
|
35
|
+
name: "SQL Injection",
|
|
36
|
+
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])"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: "sql-injection-concat",
|
|
41
|
+
name: "SQL Injection",
|
|
42
|
+
regex: /(?:query|execute)\s*\(\s*['"](?:SELECT|INSERT|UPDATE|DELETE)[^'"]*['"]\s*\+/gi,
|
|
43
|
+
fixPrompt: "Never concatenate variables into SQL strings. Use parameterized queries with placeholders."
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: "command-injection",
|
|
47
|
+
name: "Command Injection",
|
|
48
|
+
regex: /(?:exec|execSync)\s*\(\s*`[^`]*\$\{/g,
|
|
49
|
+
fixPrompt: "Avoid template literals in shell commands. Use spawn() with an array of arguments, or use a library like shell-escape."
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: "command-injection-concat",
|
|
53
|
+
name: "Command Injection",
|
|
54
|
+
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."
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: "eval-usage",
|
|
59
|
+
name: "Dangerous eval()",
|
|
60
|
+
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 entirely."
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: "new-function",
|
|
65
|
+
name: "Dangerous Function Constructor",
|
|
66
|
+
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."
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: "innerhtml-variable",
|
|
71
|
+
name: "XSS via innerHTML",
|
|
72
|
+
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."
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: "react-dangerous-html",
|
|
77
|
+
name: "React XSS Risk",
|
|
78
|
+
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."
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: "weak-hash-md5",
|
|
83
|
+
name: "Weak Hash (MD5)",
|
|
84
|
+
regex: /createHash\s*\(\s*['"`]md5['"`]\s*\)/g,
|
|
85
|
+
fixPrompt: "MD5 is broken. Use crypto.createHash('sha256'). For passwords, use bcrypt or argon2."
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
id: "weak-hash-sha1",
|
|
89
|
+
name: "Weak Hash (SHA1)",
|
|
90
|
+
regex: /createHash\s*\(\s*['"`]sha1['"`]\s*\)/g,
|
|
91
|
+
fixPrompt: "SHA1 is deprecated for security. Use crypto.createHash('sha256'). For passwords, use bcrypt or argon2."
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: "ssl-disabled",
|
|
95
|
+
name: "SSL Verification Disabled",
|
|
96
|
+
regex: /rejectUnauthorized\s*:\s*false/g,
|
|
97
|
+
fixPrompt: "Disabling SSL verification allows man-in-the-middle attacks. Remove this or set to true."
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
id: "cors-wildcard",
|
|
101
|
+
name: "CORS Allows All Origins",
|
|
102
|
+
regex: /['"`]Access-Control-Allow-Origin['"`]\s*[,:]\s*['"`]\*['"`]/g,
|
|
103
|
+
fixPrompt: "CORS wildcard (*) allows any website to make requests. Specify allowed origins explicitly."
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: "path-traversal",
|
|
107
|
+
name: "Path Traversal Risk",
|
|
108
|
+
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."
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
id: "nosql-where",
|
|
113
|
+
name: "NoSQL Injection ($where)",
|
|
114
|
+
regex: /\.(find|findOne)\s*\(\s*\{[^}]*\$where\s*:/g,
|
|
115
|
+
fixPrompt: "$where executes JavaScript and enables NoSQL injection. Use standard MongoDB query operators."
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
id: "pickle-load",
|
|
119
|
+
name: "Insecure Pickle (Python)",
|
|
120
|
+
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."
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
id: "python-shell",
|
|
125
|
+
name: "Shell Injection (Python)",
|
|
126
|
+
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."
|
|
128
|
+
}
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
// src/scanner.ts
|
|
132
|
+
var SUPPORTED_EXTENSIONS = [".js", ".ts", ".jsx", ".tsx", ".py", ".mjs", ".cjs"];
|
|
133
|
+
var IGNORED_DIRS = [
|
|
134
|
+
"node_modules",
|
|
135
|
+
".git",
|
|
136
|
+
".next",
|
|
137
|
+
"dist",
|
|
138
|
+
"build",
|
|
139
|
+
".venv",
|
|
140
|
+
"__pycache__",
|
|
141
|
+
"coverage",
|
|
142
|
+
".turbo"
|
|
143
|
+
];
|
|
144
|
+
function collectFiles(dir, files = []) {
|
|
145
|
+
let stat;
|
|
146
|
+
try {
|
|
147
|
+
stat = statSync(dir);
|
|
148
|
+
} catch {
|
|
149
|
+
return files;
|
|
150
|
+
}
|
|
151
|
+
if (stat.isFile()) {
|
|
152
|
+
const ext = extname(dir).toLowerCase();
|
|
153
|
+
if (SUPPORTED_EXTENSIONS.includes(ext)) {
|
|
154
|
+
files.push(dir);
|
|
155
|
+
}
|
|
156
|
+
return files;
|
|
157
|
+
}
|
|
158
|
+
let entries;
|
|
159
|
+
try {
|
|
160
|
+
entries = readdirSync(dir);
|
|
161
|
+
} catch {
|
|
162
|
+
return files;
|
|
163
|
+
}
|
|
164
|
+
for (const entry of entries) {
|
|
165
|
+
const fullPath = join(dir, entry);
|
|
166
|
+
try {
|
|
167
|
+
const entryStat = statSync(fullPath);
|
|
168
|
+
if (entryStat.isDirectory()) {
|
|
169
|
+
if (!IGNORED_DIRS.includes(entry) && !entry.startsWith(".")) {
|
|
170
|
+
collectFiles(fullPath, files);
|
|
171
|
+
}
|
|
172
|
+
} else if (entryStat.isFile()) {
|
|
173
|
+
const ext = extname(entry).toLowerCase();
|
|
174
|
+
if (SUPPORTED_EXTENSIONS.includes(ext)) {
|
|
175
|
+
files.push(fullPath);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
} catch {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return files;
|
|
183
|
+
}
|
|
184
|
+
function getLineNumber(content, matchIndex) {
|
|
185
|
+
const lines = content.substring(0, matchIndex).split(`
|
|
186
|
+
`);
|
|
187
|
+
return lines.length;
|
|
188
|
+
}
|
|
189
|
+
function scanFile(filePath) {
|
|
190
|
+
const issues = [];
|
|
191
|
+
let content;
|
|
192
|
+
try {
|
|
193
|
+
content = readFileSync(filePath, "utf-8");
|
|
194
|
+
} catch {
|
|
195
|
+
return issues;
|
|
196
|
+
}
|
|
197
|
+
for (const pattern of securityPatterns) {
|
|
198
|
+
pattern.regex.lastIndex = 0;
|
|
199
|
+
let match;
|
|
200
|
+
while ((match = pattern.regex.exec(content)) !== null) {
|
|
201
|
+
const line = getLineNumber(content, match.index);
|
|
202
|
+
const matchText = match[0];
|
|
203
|
+
issues.push({
|
|
204
|
+
file: filePath,
|
|
205
|
+
line,
|
|
206
|
+
patternId: pattern.id,
|
|
207
|
+
patternName: pattern.name,
|
|
208
|
+
fixPrompt: pattern.fixPrompt,
|
|
209
|
+
match: matchText.length > 60 ? matchText.substring(0, 60) + "..." : matchText
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return issues;
|
|
214
|
+
}
|
|
215
|
+
function scanFiles(dir) {
|
|
216
|
+
const files = collectFiles(dir);
|
|
217
|
+
const allIssues = [];
|
|
218
|
+
for (const file of files) {
|
|
219
|
+
const issues = scanFile(file);
|
|
220
|
+
allIssues.push(...issues);
|
|
221
|
+
}
|
|
222
|
+
return allIssues;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// src/prompter.ts
|
|
226
|
+
function formatAgentPrompt(issues) {
|
|
227
|
+
if (!issues || issues.length === 0) {
|
|
228
|
+
return "";
|
|
229
|
+
}
|
|
230
|
+
const lines = [
|
|
231
|
+
"═══════════════════════════════════════════════════════════════",
|
|
232
|
+
" HEY AGENT, PLEASE FIX THE FOLLOWING SECURITY ISSUES:",
|
|
233
|
+
"═══════════════════════════════════════════════════════════════",
|
|
234
|
+
""
|
|
235
|
+
];
|
|
236
|
+
issues.forEach((issue, index) => {
|
|
237
|
+
lines.push(`[TASK ${index + 1}] Fix ${issue.patternName} in ${issue.file} at line ${issue.line}`);
|
|
238
|
+
lines.push(`[FOUND]: ${issue.match}`);
|
|
239
|
+
lines.push(`[INSTRUCTION]: ${issue.fixPrompt}`);
|
|
240
|
+
lines.push("");
|
|
241
|
+
});
|
|
242
|
+
lines.push("═══════════════════════════════════════════════════════════════");
|
|
243
|
+
lines.push(` Total issues: ${issues.length}`);
|
|
244
|
+
lines.push("═══════════════════════════════════════════════════════════════");
|
|
245
|
+
return lines.join(`
|
|
246
|
+
`);
|
|
247
|
+
}
|
|
248
|
+
function generateSummary(issues) {
|
|
249
|
+
const summary = {};
|
|
250
|
+
for (const issue of issues) {
|
|
251
|
+
if (!summary[issue.patternName]) {
|
|
252
|
+
summary[issue.patternName] = 0;
|
|
253
|
+
}
|
|
254
|
+
summary[issue.patternName]++;
|
|
255
|
+
}
|
|
256
|
+
return summary;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// src/init.ts
|
|
260
|
+
import { writeFileSync, existsSync, readFileSync as readFileSync2 } from "fs";
|
|
261
|
+
import { join as join2 } from "path";
|
|
262
|
+
var CURSORRULES_CONTENT = `# Vibe Shield Security Rules
|
|
263
|
+
|
|
264
|
+
Before marking a task as done, run \`npx vibe-shield\`. If issues are found, follow the [INSTRUCTION] provided in the output to fix them.
|
|
265
|
+
|
|
266
|
+
## Security Checklist
|
|
267
|
+
- [ ] No hardcoded secrets (API keys, passwords, tokens)
|
|
268
|
+
- [ ] No dangerous code execution (eval, shell injection)
|
|
269
|
+
- [ ] No SQL injection vulnerabilities (use parameterized queries)
|
|
270
|
+
- [ ] All secrets stored in environment variables
|
|
271
|
+
- [ ] HTTPS used for all external URLs
|
|
272
|
+
`;
|
|
273
|
+
function initVibeShield(dir) {
|
|
274
|
+
const cursorrulesPath = join2(dir, ".cursorrules");
|
|
275
|
+
if (existsSync(cursorrulesPath)) {
|
|
276
|
+
try {
|
|
277
|
+
const existingContent = readFileSync2(cursorrulesPath, "utf-8");
|
|
278
|
+
if (existingContent.includes("vibe-shield") || existingContent.includes("Vibe Shield")) {
|
|
279
|
+
return {
|
|
280
|
+
success: false,
|
|
281
|
+
message: "Vibe Shield rules already exist in .cursorrules",
|
|
282
|
+
path: cursorrulesPath
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
const newContent = existingContent + `
|
|
286
|
+
|
|
287
|
+
` + CURSORRULES_CONTENT;
|
|
288
|
+
writeFileSync(cursorrulesPath, newContent, "utf-8");
|
|
289
|
+
return {
|
|
290
|
+
success: true,
|
|
291
|
+
message: "Vibe Shield rules appended to existing .cursorrules",
|
|
292
|
+
path: cursorrulesPath
|
|
293
|
+
};
|
|
294
|
+
} catch (err) {
|
|
295
|
+
return {
|
|
296
|
+
success: false,
|
|
297
|
+
message: `Failed to update .cursorrules: ${err instanceof Error ? err.message : "Unknown error"}`,
|
|
298
|
+
path: cursorrulesPath
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
try {
|
|
303
|
+
writeFileSync(cursorrulesPath, CURSORRULES_CONTENT, "utf-8");
|
|
304
|
+
return {
|
|
305
|
+
success: true,
|
|
306
|
+
message: ".cursorrules created successfully!",
|
|
307
|
+
path: cursorrulesPath
|
|
308
|
+
};
|
|
309
|
+
} catch (err) {
|
|
310
|
+
return {
|
|
311
|
+
success: false,
|
|
312
|
+
message: `Failed to create .cursorrules: ${err instanceof Error ? err.message : "Unknown error"}`,
|
|
313
|
+
path: cursorrulesPath
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// src/cli.ts
|
|
319
|
+
var VERSION = "1.0.0";
|
|
320
|
+
var colors = {
|
|
321
|
+
reset: "\x1B[0m",
|
|
322
|
+
cyan: "\x1B[36m",
|
|
323
|
+
green: "\x1B[32m",
|
|
324
|
+
red: "\x1B[31m",
|
|
325
|
+
yellow: "\x1B[33m",
|
|
326
|
+
dim: "\x1B[2m",
|
|
327
|
+
bold: "\x1B[1m"
|
|
328
|
+
};
|
|
329
|
+
function printBanner() {
|
|
330
|
+
console.log(colors.cyan + `
|
|
331
|
+
╦ ╦╦╔╗ ╔═╗ ╔═╗╦ ╦╦╔═╗╦ ╔╦╗
|
|
332
|
+
╚╗╔╝║╠╩╗║╣ ╚═╗╠═╣║║╣ ║ ║║
|
|
333
|
+
╚╝ ╩╚═╝╚═╝ ╚═╝╩ ╩╩╚═╝╩═╝═╩╝
|
|
334
|
+
` + colors.reset);
|
|
335
|
+
console.log(colors.dim + ` v${VERSION} - Security scanner for vibe coders
|
|
336
|
+
` + colors.reset);
|
|
337
|
+
}
|
|
338
|
+
function printHelp() {
|
|
339
|
+
printBanner();
|
|
340
|
+
console.log("Usage:");
|
|
341
|
+
console.log(colors.dim + ` vibe-shield [command] [options]
|
|
342
|
+
` + colors.reset);
|
|
343
|
+
console.log("Commands:");
|
|
344
|
+
console.log(colors.green + " scan [path]" + colors.reset + colors.dim + " Scan for security issues (default: .)" + colors.reset);
|
|
345
|
+
console.log(colors.green + " init" + colors.reset + colors.dim + " Create .cursorrules for AI agent integration" + colors.reset);
|
|
346
|
+
console.log(colors.green + " help" + colors.reset + colors.dim + " Show this help message" + colors.reset);
|
|
347
|
+
console.log(colors.green + " version" + colors.reset + colors.dim + ` Show version number
|
|
348
|
+
` + colors.reset);
|
|
349
|
+
console.log("Examples:");
|
|
350
|
+
console.log(colors.dim + " npx vibe-shield" + colors.reset);
|
|
351
|
+
console.log(colors.dim + " npx vibe-shield scan ./src" + colors.reset);
|
|
352
|
+
console.log(colors.dim + ` npx vibe-shield init
|
|
353
|
+
` + colors.reset);
|
|
354
|
+
}
|
|
355
|
+
function printVersion() {
|
|
356
|
+
console.log(`vibe-shield v${VERSION}`);
|
|
357
|
+
}
|
|
358
|
+
function runInit(dir) {
|
|
359
|
+
printBanner();
|
|
360
|
+
console.log(colors.yellow + `Initializing vibe-shield...
|
|
361
|
+
` + colors.reset);
|
|
362
|
+
const result = initVibeShield(dir);
|
|
363
|
+
if (result.success) {
|
|
364
|
+
console.log(colors.green + "✓ " + colors.reset + result.message);
|
|
365
|
+
console.log(colors.dim + ` Created: ${result.path}
|
|
366
|
+
` + colors.reset);
|
|
367
|
+
console.log("Your AI agent will now run vibe-shield before completing tasks.");
|
|
368
|
+
console.log(colors.dim + `Happy vibe coding!
|
|
369
|
+
` + colors.reset);
|
|
370
|
+
process.exit(0);
|
|
371
|
+
} else {
|
|
372
|
+
console.log(colors.red + "✗ " + colors.reset + result.message);
|
|
373
|
+
process.exit(1);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
function runScan(dir) {
|
|
377
|
+
printBanner();
|
|
378
|
+
console.log(colors.yellow + `Scanning ${dir}...
|
|
379
|
+
` + colors.reset);
|
|
380
|
+
const issues = scanFiles(dir);
|
|
381
|
+
if (issues.length === 0) {
|
|
382
|
+
console.log(colors.green + colors.bold + "✓ SAFE" + colors.reset);
|
|
383
|
+
console.log(colors.dim + `No security issues detected. Ship it!
|
|
384
|
+
` + colors.reset);
|
|
385
|
+
process.exit(0);
|
|
386
|
+
}
|
|
387
|
+
const summary = generateSummary(issues);
|
|
388
|
+
console.log(colors.red + colors.bold + `✗ Found ${issues.length} security issue${issues.length > 1 ? "s" : ""}
|
|
389
|
+
` + colors.reset);
|
|
390
|
+
console.log("Summary:");
|
|
391
|
+
for (const [pattern, count] of Object.entries(summary)) {
|
|
392
|
+
console.log(colors.dim + ` • ${pattern}: ${count}` + colors.reset);
|
|
393
|
+
}
|
|
394
|
+
console.log("");
|
|
395
|
+
const agentPrompt = formatAgentPrompt(issues);
|
|
396
|
+
console.log(colors.yellow + agentPrompt + colors.reset);
|
|
397
|
+
console.log("");
|
|
398
|
+
process.exit(1);
|
|
399
|
+
}
|
|
400
|
+
var args = process.argv.slice(2);
|
|
401
|
+
var command = args[0] || "scan";
|
|
402
|
+
var targetDir = args[1] || process.cwd();
|
|
403
|
+
switch (command) {
|
|
404
|
+
case "init":
|
|
405
|
+
runInit(targetDir === process.cwd() ? process.cwd() : targetDir);
|
|
406
|
+
break;
|
|
407
|
+
case "scan":
|
|
408
|
+
runScan(targetDir);
|
|
409
|
+
break;
|
|
410
|
+
case "help":
|
|
411
|
+
case "--help":
|
|
412
|
+
case "-h":
|
|
413
|
+
printHelp();
|
|
414
|
+
process.exit(0);
|
|
415
|
+
break;
|
|
416
|
+
case "version":
|
|
417
|
+
case "--version":
|
|
418
|
+
case "-v":
|
|
419
|
+
printVersion();
|
|
420
|
+
process.exit(0);
|
|
421
|
+
break;
|
|
422
|
+
default:
|
|
423
|
+
if (command.startsWith(".") || command.startsWith("/")) {
|
|
424
|
+
runScan(command);
|
|
425
|
+
} else {
|
|
426
|
+
console.log(colors.red + `Unknown command: ${command}` + colors.reset);
|
|
427
|
+
console.log(colors.dim + `Run "vibe-shield help" for usage.
|
|
428
|
+
` + colors.reset);
|
|
429
|
+
process.exit(1);
|
|
430
|
+
}
|
|
431
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { scanFiles } from "./scanner";
|
|
2
|
+
export { formatAgentPrompt, generateSummary } from "./prompter";
|
|
3
|
+
export { initVibeShield } from "./init";
|
|
4
|
+
export { securityPatterns } from "./patterns";
|
|
5
|
+
export type { SecurityPattern, SecurityIssue, InitResult, IssueSummary } from "./types";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
// src/scanner.ts
|
|
2
|
+
import { readFileSync, readdirSync, statSync } from "fs";
|
|
3
|
+
import { join, extname } from "path";
|
|
4
|
+
|
|
5
|
+
// src/patterns.ts
|
|
6
|
+
var securityPatterns = [
|
|
7
|
+
{
|
|
8
|
+
id: "hardcoded-secret",
|
|
9
|
+
name: "Hardcoded Secret",
|
|
10
|
+
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 it to .env and use process.env.YOUR_SECRET. Add .env to .gitignore."
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
id: "hardcoded-password",
|
|
15
|
+
name: "Hardcoded Password",
|
|
16
|
+
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."
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: "jwt-secret-inline",
|
|
27
|
+
name: "Hardcoded JWT Secret",
|
|
28
|
+
regex: /jwt\.sign\s*\([^)]+,\s*['"`][^'"`]{8,}['"`]/gi,
|
|
29
|
+
fixPrompt: "Move JWT secret to an environment variable. Hardcoded secrets get committed to version control."
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: "sql-injection-template",
|
|
33
|
+
name: "SQL Injection",
|
|
34
|
+
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])"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: "sql-injection-concat",
|
|
39
|
+
name: "SQL Injection",
|
|
40
|
+
regex: /(?:query|execute)\s*\(\s*['"](?:SELECT|INSERT|UPDATE|DELETE)[^'"]*['"]\s*\+/gi,
|
|
41
|
+
fixPrompt: "Never concatenate variables into SQL strings. Use parameterized queries with placeholders."
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: "command-injection",
|
|
45
|
+
name: "Command Injection",
|
|
46
|
+
regex: /(?:exec|execSync)\s*\(\s*`[^`]*\$\{/g,
|
|
47
|
+
fixPrompt: "Avoid template literals in shell commands. Use spawn() with an array of arguments, or use a library like shell-escape."
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: "command-injection-concat",
|
|
51
|
+
name: "Command Injection",
|
|
52
|
+
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."
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: "eval-usage",
|
|
57
|
+
name: "Dangerous eval()",
|
|
58
|
+
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 entirely."
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: "new-function",
|
|
63
|
+
name: "Dangerous Function Constructor",
|
|
64
|
+
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."
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: "innerhtml-variable",
|
|
69
|
+
name: "XSS via innerHTML",
|
|
70
|
+
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."
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: "react-dangerous-html",
|
|
75
|
+
name: "React XSS Risk",
|
|
76
|
+
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."
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: "weak-hash-md5",
|
|
81
|
+
name: "Weak Hash (MD5)",
|
|
82
|
+
regex: /createHash\s*\(\s*['"`]md5['"`]\s*\)/g,
|
|
83
|
+
fixPrompt: "MD5 is broken. Use crypto.createHash('sha256'). For passwords, use bcrypt or argon2."
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: "weak-hash-sha1",
|
|
87
|
+
name: "Weak Hash (SHA1)",
|
|
88
|
+
regex: /createHash\s*\(\s*['"`]sha1['"`]\s*\)/g,
|
|
89
|
+
fixPrompt: "SHA1 is deprecated for security. Use crypto.createHash('sha256'). For passwords, use bcrypt or argon2."
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
id: "ssl-disabled",
|
|
93
|
+
name: "SSL Verification Disabled",
|
|
94
|
+
regex: /rejectUnauthorized\s*:\s*false/g,
|
|
95
|
+
fixPrompt: "Disabling SSL verification allows man-in-the-middle attacks. Remove this or set to true."
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
id: "cors-wildcard",
|
|
99
|
+
name: "CORS Allows All Origins",
|
|
100
|
+
regex: /['"`]Access-Control-Allow-Origin['"`]\s*[,:]\s*['"`]\*['"`]/g,
|
|
101
|
+
fixPrompt: "CORS wildcard (*) allows any website to make requests. Specify allowed origins explicitly."
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: "path-traversal",
|
|
105
|
+
name: "Path Traversal Risk",
|
|
106
|
+
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."
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
id: "nosql-where",
|
|
111
|
+
name: "NoSQL Injection ($where)",
|
|
112
|
+
regex: /\.(find|findOne)\s*\(\s*\{[^}]*\$where\s*:/g,
|
|
113
|
+
fixPrompt: "$where executes JavaScript and enables NoSQL injection. Use standard MongoDB query operators."
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
id: "pickle-load",
|
|
117
|
+
name: "Insecure Pickle (Python)",
|
|
118
|
+
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."
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
id: "python-shell",
|
|
123
|
+
name: "Shell Injection (Python)",
|
|
124
|
+
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."
|
|
126
|
+
}
|
|
127
|
+
];
|
|
128
|
+
|
|
129
|
+
// src/scanner.ts
|
|
130
|
+
var SUPPORTED_EXTENSIONS = [".js", ".ts", ".jsx", ".tsx", ".py", ".mjs", ".cjs"];
|
|
131
|
+
var IGNORED_DIRS = [
|
|
132
|
+
"node_modules",
|
|
133
|
+
".git",
|
|
134
|
+
".next",
|
|
135
|
+
"dist",
|
|
136
|
+
"build",
|
|
137
|
+
".venv",
|
|
138
|
+
"__pycache__",
|
|
139
|
+
"coverage",
|
|
140
|
+
".turbo"
|
|
141
|
+
];
|
|
142
|
+
function collectFiles(dir, files = []) {
|
|
143
|
+
let stat;
|
|
144
|
+
try {
|
|
145
|
+
stat = statSync(dir);
|
|
146
|
+
} catch {
|
|
147
|
+
return files;
|
|
148
|
+
}
|
|
149
|
+
if (stat.isFile()) {
|
|
150
|
+
const ext = extname(dir).toLowerCase();
|
|
151
|
+
if (SUPPORTED_EXTENSIONS.includes(ext)) {
|
|
152
|
+
files.push(dir);
|
|
153
|
+
}
|
|
154
|
+
return files;
|
|
155
|
+
}
|
|
156
|
+
let entries;
|
|
157
|
+
try {
|
|
158
|
+
entries = readdirSync(dir);
|
|
159
|
+
} catch {
|
|
160
|
+
return files;
|
|
161
|
+
}
|
|
162
|
+
for (const entry of entries) {
|
|
163
|
+
const fullPath = join(dir, entry);
|
|
164
|
+
try {
|
|
165
|
+
const entryStat = statSync(fullPath);
|
|
166
|
+
if (entryStat.isDirectory()) {
|
|
167
|
+
if (!IGNORED_DIRS.includes(entry) && !entry.startsWith(".")) {
|
|
168
|
+
collectFiles(fullPath, files);
|
|
169
|
+
}
|
|
170
|
+
} else if (entryStat.isFile()) {
|
|
171
|
+
const ext = extname(entry).toLowerCase();
|
|
172
|
+
if (SUPPORTED_EXTENSIONS.includes(ext)) {
|
|
173
|
+
files.push(fullPath);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
} catch {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return files;
|
|
181
|
+
}
|
|
182
|
+
function getLineNumber(content, matchIndex) {
|
|
183
|
+
const lines = content.substring(0, matchIndex).split(`
|
|
184
|
+
`);
|
|
185
|
+
return lines.length;
|
|
186
|
+
}
|
|
187
|
+
function scanFile(filePath) {
|
|
188
|
+
const issues = [];
|
|
189
|
+
let content;
|
|
190
|
+
try {
|
|
191
|
+
content = readFileSync(filePath, "utf-8");
|
|
192
|
+
} catch {
|
|
193
|
+
return issues;
|
|
194
|
+
}
|
|
195
|
+
for (const pattern of securityPatterns) {
|
|
196
|
+
pattern.regex.lastIndex = 0;
|
|
197
|
+
let match;
|
|
198
|
+
while ((match = pattern.regex.exec(content)) !== null) {
|
|
199
|
+
const line = getLineNumber(content, match.index);
|
|
200
|
+
const matchText = match[0];
|
|
201
|
+
issues.push({
|
|
202
|
+
file: filePath,
|
|
203
|
+
line,
|
|
204
|
+
patternId: pattern.id,
|
|
205
|
+
patternName: pattern.name,
|
|
206
|
+
fixPrompt: pattern.fixPrompt,
|
|
207
|
+
match: matchText.length > 60 ? matchText.substring(0, 60) + "..." : matchText
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return issues;
|
|
212
|
+
}
|
|
213
|
+
function scanFiles(dir) {
|
|
214
|
+
const files = collectFiles(dir);
|
|
215
|
+
const allIssues = [];
|
|
216
|
+
for (const file of files) {
|
|
217
|
+
const issues = scanFile(file);
|
|
218
|
+
allIssues.push(...issues);
|
|
219
|
+
}
|
|
220
|
+
return allIssues;
|
|
221
|
+
}
|
|
222
|
+
// src/prompter.ts
|
|
223
|
+
function formatAgentPrompt(issues) {
|
|
224
|
+
if (!issues || issues.length === 0) {
|
|
225
|
+
return "";
|
|
226
|
+
}
|
|
227
|
+
const lines = [
|
|
228
|
+
"═══════════════════════════════════════════════════════════════",
|
|
229
|
+
" HEY AGENT, PLEASE FIX THE FOLLOWING SECURITY ISSUES:",
|
|
230
|
+
"═══════════════════════════════════════════════════════════════",
|
|
231
|
+
""
|
|
232
|
+
];
|
|
233
|
+
issues.forEach((issue, index) => {
|
|
234
|
+
lines.push(`[TASK ${index + 1}] Fix ${issue.patternName} in ${issue.file} at line ${issue.line}`);
|
|
235
|
+
lines.push(`[FOUND]: ${issue.match}`);
|
|
236
|
+
lines.push(`[INSTRUCTION]: ${issue.fixPrompt}`);
|
|
237
|
+
lines.push("");
|
|
238
|
+
});
|
|
239
|
+
lines.push("═══════════════════════════════════════════════════════════════");
|
|
240
|
+
lines.push(` Total issues: ${issues.length}`);
|
|
241
|
+
lines.push("═══════════════════════════════════════════════════════════════");
|
|
242
|
+
return lines.join(`
|
|
243
|
+
`);
|
|
244
|
+
}
|
|
245
|
+
function generateSummary(issues) {
|
|
246
|
+
const summary = {};
|
|
247
|
+
for (const issue of issues) {
|
|
248
|
+
if (!summary[issue.patternName]) {
|
|
249
|
+
summary[issue.patternName] = 0;
|
|
250
|
+
}
|
|
251
|
+
summary[issue.patternName]++;
|
|
252
|
+
}
|
|
253
|
+
return summary;
|
|
254
|
+
}
|
|
255
|
+
// src/init.ts
|
|
256
|
+
import { writeFileSync, existsSync, readFileSync as readFileSync2 } from "fs";
|
|
257
|
+
import { join as join2 } from "path";
|
|
258
|
+
var CURSORRULES_CONTENT = `# Vibe Shield Security Rules
|
|
259
|
+
|
|
260
|
+
Before marking a task as done, run \`npx vibe-shield\`. If issues are found, follow the [INSTRUCTION] provided in the output to fix them.
|
|
261
|
+
|
|
262
|
+
## Security Checklist
|
|
263
|
+
- [ ] No hardcoded secrets (API keys, passwords, tokens)
|
|
264
|
+
- [ ] No dangerous code execution (eval, shell injection)
|
|
265
|
+
- [ ] No SQL injection vulnerabilities (use parameterized queries)
|
|
266
|
+
- [ ] All secrets stored in environment variables
|
|
267
|
+
- [ ] HTTPS used for all external URLs
|
|
268
|
+
`;
|
|
269
|
+
function initVibeShield(dir) {
|
|
270
|
+
const cursorrulesPath = join2(dir, ".cursorrules");
|
|
271
|
+
if (existsSync(cursorrulesPath)) {
|
|
272
|
+
try {
|
|
273
|
+
const existingContent = readFileSync2(cursorrulesPath, "utf-8");
|
|
274
|
+
if (existingContent.includes("vibe-shield") || existingContent.includes("Vibe Shield")) {
|
|
275
|
+
return {
|
|
276
|
+
success: false,
|
|
277
|
+
message: "Vibe Shield rules already exist in .cursorrules",
|
|
278
|
+
path: cursorrulesPath
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
const newContent = existingContent + `
|
|
282
|
+
|
|
283
|
+
` + CURSORRULES_CONTENT;
|
|
284
|
+
writeFileSync(cursorrulesPath, newContent, "utf-8");
|
|
285
|
+
return {
|
|
286
|
+
success: true,
|
|
287
|
+
message: "Vibe Shield rules appended to existing .cursorrules",
|
|
288
|
+
path: cursorrulesPath
|
|
289
|
+
};
|
|
290
|
+
} catch (err) {
|
|
291
|
+
return {
|
|
292
|
+
success: false,
|
|
293
|
+
message: `Failed to update .cursorrules: ${err instanceof Error ? err.message : "Unknown error"}`,
|
|
294
|
+
path: cursorrulesPath
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
try {
|
|
299
|
+
writeFileSync(cursorrulesPath, CURSORRULES_CONTENT, "utf-8");
|
|
300
|
+
return {
|
|
301
|
+
success: true,
|
|
302
|
+
message: ".cursorrules created successfully!",
|
|
303
|
+
path: cursorrulesPath
|
|
304
|
+
};
|
|
305
|
+
} catch (err) {
|
|
306
|
+
return {
|
|
307
|
+
success: false,
|
|
308
|
+
message: `Failed to create .cursorrules: ${err instanceof Error ? err.message : "Unknown error"}`,
|
|
309
|
+
path: cursorrulesPath
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
export {
|
|
314
|
+
securityPatterns,
|
|
315
|
+
scanFiles,
|
|
316
|
+
initVibeShield,
|
|
317
|
+
generateSummary,
|
|
318
|
+
formatAgentPrompt
|
|
319
|
+
};
|
package/dist/init.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { SecurityIssue, IssueSummary } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Format issues into an "Agent Protocol" string that AI agents can read and act on.
|
|
4
|
+
*/
|
|
5
|
+
export declare function formatAgentPrompt(issues: SecurityIssue[]): string;
|
|
6
|
+
/**
|
|
7
|
+
* Generate a summary of issues by type.
|
|
8
|
+
*/
|
|
9
|
+
export declare function generateSummary(issues: SecurityIssue[]): IssueSummary;
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface SecurityPattern {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
regex: RegExp;
|
|
5
|
+
fixPrompt: string;
|
|
6
|
+
}
|
|
7
|
+
export interface SecurityIssue {
|
|
8
|
+
file: string;
|
|
9
|
+
line: number;
|
|
10
|
+
patternId: string;
|
|
11
|
+
patternName: string;
|
|
12
|
+
fixPrompt: string;
|
|
13
|
+
match: string;
|
|
14
|
+
}
|
|
15
|
+
export interface InitResult {
|
|
16
|
+
success: boolean;
|
|
17
|
+
message: string;
|
|
18
|
+
path: string;
|
|
19
|
+
}
|
|
20
|
+
export interface IssueSummary {
|
|
21
|
+
[patternName: string]: number;
|
|
22
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vibe-shield",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Security scanner for vibe coders - find and fix AI-generated security issues",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"vibe-shield": "./dist/cli.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"dev": "bun run src/cli.ts",
|
|
16
|
+
"build": "bun build src/cli.ts --outdir dist --target node && bun build src/index.ts --outdir dist --target node && bun x tsc --emitDeclarationOnly",
|
|
17
|
+
"prepublishOnly": "bun run build",
|
|
18
|
+
"typecheck": "tsc --noEmit",
|
|
19
|
+
"test": "bun test"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"security",
|
|
23
|
+
"scanner",
|
|
24
|
+
"ai",
|
|
25
|
+
"vibe-coding",
|
|
26
|
+
"cli",
|
|
27
|
+
"cursor",
|
|
28
|
+
"claude",
|
|
29
|
+
"devtools",
|
|
30
|
+
"linter",
|
|
31
|
+
"vulnerabilities"
|
|
32
|
+
],
|
|
33
|
+
"author": "",
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "git+https://github.com/egmzy/vibe-shield.git"
|
|
38
|
+
},
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/egmzy/vibe-shield/issues"
|
|
41
|
+
},
|
|
42
|
+
"homepage": "https://github.com/egmzy/vibe-shield#readme",
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=18.0.0"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/bun": "latest",
|
|
48
|
+
"@types/node": "^20.10.0",
|
|
49
|
+
"typescript": "^5.3.0"
|
|
50
|
+
}
|
|
51
|
+
}
|