vibe-secure 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/README.md +23 -0
- package/index.js +29 -7
- package/package.json +7 -2
- package/src/checks/ai/analysis.js +70 -0
- package/src/checks/ast/eval.js +53 -0
- package/src/core/parser.js +51 -0
- package/src/{scanner.js ā core/scanner.js} +2 -1
package/README.md
CHANGED
|
@@ -60,6 +60,29 @@ What it Checks
|
|
|
60
60
|
| Auth | Flags API routes like app.delete('/user/:id') that appear to have no middleware. |
|
|
61
61
|
| SQL Injection | Flags SELECT statements using string concatenation or template literals. |
|
|
62
62
|
| XSS | Flags dangerouslySetInnerHTML, javascript: links, and eval(). |
|
|
63
|
+
| **AI Analysis** | (Optional) Uses **Google Gemini 2.0 Flash** to find logic flaws and subtle bugs. |
|
|
64
|
+
|
|
65
|
+
AI-Powered Analysis (Experimental)
|
|
66
|
+
----------------------------------
|
|
67
|
+
|
|
68
|
+
Vibe Secure now supports **AI-powered security audits** using Google Gemini. This goes beyond regex to find logic bugs, such as missing permission checks or authentication bypasses.
|
|
69
|
+
|
|
70
|
+
**Requirements:**
|
|
71
|
+
1. Get a [Google Gemini API Key](https://aistudio.google.com/).
|
|
72
|
+
2. Set the environment variable:
|
|
73
|
+
```bash
|
|
74
|
+
export GEMINI_API_KEY=your_key_here
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**Usage:**
|
|
78
|
+
Run with the `--ai` flag:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
vibe-secure . --ai
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
> **Note:** This feature sends your code snippet (up to 10k chars) to Google's AI for analysis. Use with caution on private codebases.
|
|
85
|
+
|
|
63
86
|
|
|
64
87
|
Development
|
|
65
88
|
-----------
|
package/index.js
CHANGED
|
@@ -3,12 +3,15 @@
|
|
|
3
3
|
const { Command } = require('commander');
|
|
4
4
|
const chalk = require('chalk');
|
|
5
5
|
const figlet = require('figlet');
|
|
6
|
-
const { scanFolder } = require('./src/scanner');
|
|
6
|
+
const { scanFolder } = require('./src/core/scanner');
|
|
7
7
|
const { checkForSecrets } = require('./src/checks/secrets');
|
|
8
|
+
|
|
8
9
|
const { checkForDatabaseUrls } = require('./src/checks/dbCheck');
|
|
9
10
|
const { checkForUnsafeEndpoints } = require('./src/checks/endpointCheck');
|
|
10
11
|
const { checkForSqlInjection } = require('./src/checks/sqlInjection.js');
|
|
11
12
|
const { checkForDangerousReact } = require('./src/checks/unsafeReact.js');
|
|
13
|
+
const { checkForEval } = require('./src/checks/ast/eval.js');
|
|
14
|
+
const { aiSecurityReview } = require('./src/checks/ai/analysis.js');
|
|
12
15
|
const fs = require('fs').promises;
|
|
13
16
|
const path = require('path');
|
|
14
17
|
|
|
@@ -17,15 +20,23 @@ const program = new Command();
|
|
|
17
20
|
console.log(chalk.cyan(figlet.textSync('Vibe Secure', { horizontalLayout: 'full' })));
|
|
18
21
|
|
|
19
22
|
program
|
|
20
|
-
.version('1.0.
|
|
23
|
+
.version('1.0.1')
|
|
21
24
|
.description('Security scanner for vibe-coded AI applications')
|
|
22
25
|
.argument('[directory]', 'Directory to scan', '.')
|
|
23
|
-
.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
.option('--ai', 'Enable AI-powered analysis (requires GEMINI_API_KEY)')
|
|
27
|
+
.action(async (directory, options) => {
|
|
28
|
+
const targetPath = path.resolve(directory);
|
|
29
|
+
console.log(chalk.blue(`\nš Scanning: ${targetPath}\n`));
|
|
26
30
|
|
|
27
31
|
try {
|
|
28
|
-
|
|
32
|
+
let files = [];
|
|
33
|
+
const stat = await fs.stat(targetPath);
|
|
34
|
+
|
|
35
|
+
if (stat.isFile()) {
|
|
36
|
+
files = [targetPath];
|
|
37
|
+
} else {
|
|
38
|
+
files = await scanFolder(targetPath);
|
|
39
|
+
}
|
|
29
40
|
|
|
30
41
|
if (files.length === 0) {
|
|
31
42
|
console.log(chalk.yellow('ā ļø No relevant implementation files found (.js, .jsx, .ts, .tsx).'));
|
|
@@ -48,10 +59,21 @@ program
|
|
|
48
59
|
...checkForDatabaseUrls(content, relativePath),
|
|
49
60
|
...checkForUnsafeEndpoints(content, relativePath),
|
|
50
61
|
...checkForSqlInjection(content, relativePath),
|
|
51
|
-
...checkForDangerousReact(content, relativePath)
|
|
62
|
+
...checkForDangerousReact(content, relativePath),
|
|
63
|
+
...checkForEval(content, relativePath)
|
|
52
64
|
];
|
|
53
65
|
|
|
54
66
|
allIssues = [...allIssues, ...issues];
|
|
67
|
+
|
|
68
|
+
// AI Analysis (Optional)
|
|
69
|
+
if (options.ai) {
|
|
70
|
+
if (!process.env.GEMINI_API_KEY) {
|
|
71
|
+
console.warn(chalk.yellow(`\nā ļø Skipping specific file AI analysis: GEMINI_API_KEY not found.`));
|
|
72
|
+
} else {
|
|
73
|
+
const aiIssues = await aiSecurityReview(content, relativePath);
|
|
74
|
+
allIssues.push(...aiIssues);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
55
77
|
}
|
|
56
78
|
|
|
57
79
|
console.log(chalk.gray('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā'));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vibe-secure",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "The security scanner for AI-generated applications code",
|
|
5
5
|
"bin": {
|
|
6
6
|
"vibe-secure": "index.js"
|
|
@@ -20,8 +20,13 @@
|
|
|
20
20
|
"license": "MIT",
|
|
21
21
|
"type": "commonjs",
|
|
22
22
|
"dependencies": {
|
|
23
|
+
"@google/generative-ai": "^0.24.1",
|
|
23
24
|
"chalk": "^4.1.2",
|
|
24
25
|
"commander": "^14.0.2",
|
|
25
|
-
"
|
|
26
|
+
"dotenv": "^17.2.3",
|
|
27
|
+
"figlet": "^1.10.0",
|
|
28
|
+
"tree-sitter": "^0.21.1",
|
|
29
|
+
"tree-sitter-javascript": "^0.21.4",
|
|
30
|
+
"tree-sitter-typescript": "^0.21.2"
|
|
26
31
|
}
|
|
27
32
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const { GoogleGenerativeAI } = require("@google/generative-ai");
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Analyzes code using Google Gemini for security vulnerabilities
|
|
5
|
+
* @param {string} code - The code content
|
|
6
|
+
* @param {string} filePath - The path to the file
|
|
7
|
+
* @returns {Promise<Array>} - Array of found issues
|
|
8
|
+
*/
|
|
9
|
+
async function aiSecurityReview(code, filePath) {
|
|
10
|
+
const apiKey = process.env.GEMINI_API_KEY;
|
|
11
|
+
if (!apiKey) {
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const genAI = new GoogleGenerativeAI(apiKey);
|
|
16
|
+
// Using gemini-2.0-flash for speed and availability
|
|
17
|
+
const model = genAI.getGenerativeModel({ model: "gemini-2.0-flash" });
|
|
18
|
+
|
|
19
|
+
const prompt = `You are an expert security auditor. Analyze the following code for security vulnerabilities.
|
|
20
|
+
Focus on:
|
|
21
|
+
1. Injection attacks (SQL, XSS, Command Injection)
|
|
22
|
+
2. Authentication/Authorization bypasses
|
|
23
|
+
3. Hardcoded secrets not caught by regex
|
|
24
|
+
4. Exposed sensitive data
|
|
25
|
+
5. Logic flaws
|
|
26
|
+
|
|
27
|
+
Return ONLY a JSON object with this valid schema:
|
|
28
|
+
{
|
|
29
|
+
"issues": [
|
|
30
|
+
{
|
|
31
|
+
"severity": "Critical" | "High" | "Medium" | "Low",
|
|
32
|
+
"line": <number>,
|
|
33
|
+
"message": "<concise description>",
|
|
34
|
+
"fix": "<brief fix suggestion>"
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
If no issues are found, return { "issues": [] }.
|
|
40
|
+
|
|
41
|
+
Code to analyze (${filePath}):
|
|
42
|
+
${code.substring(0, 10000)}
|
|
43
|
+
`; // Truncate to avoid context limit
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const result = await model.generateContent(prompt);
|
|
47
|
+
const response = await result.response;
|
|
48
|
+
const responseText = response.text();
|
|
49
|
+
|
|
50
|
+
// Extract JSON from response (handle potential markdown blocks)
|
|
51
|
+
const jsonMatch = responseText.match(/\{.*\}/s);
|
|
52
|
+
if (!jsonMatch) return [];
|
|
53
|
+
|
|
54
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
55
|
+
|
|
56
|
+
return parsed.issues.map(issue => ({
|
|
57
|
+
file: filePath,
|
|
58
|
+
line: issue.line || 1, // Fallback
|
|
59
|
+
type: `[AI] ${issue.severity}`,
|
|
60
|
+
message: issue.message,
|
|
61
|
+
code: issue.fix || "Check logic"
|
|
62
|
+
}));
|
|
63
|
+
|
|
64
|
+
} catch (error) {
|
|
65
|
+
console.error("AI Analysis failed:", error.message);
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = { aiSecurityReview };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const parser = require('../../core/parser');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Checks for usage of eval() using Tree-sitter
|
|
5
|
+
* @param {string} content
|
|
6
|
+
* @param {string} filePath
|
|
7
|
+
* @returns {Array} issues
|
|
8
|
+
*/
|
|
9
|
+
function checkForEval(content, filePath) {
|
|
10
|
+
const issues = [];
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const tree = parser.parse(content, filePath);
|
|
14
|
+
const ext = filePath.split('.').pop();
|
|
15
|
+
const lang = ext === 'tsx' ? 'tsx' : (ext === 'ts' ? 'ts' : 'js');
|
|
16
|
+
|
|
17
|
+
// Query for function calls to 'eval'
|
|
18
|
+
// (call_expression function: (identifier) @name (#eq? @name "eval"))
|
|
19
|
+
const query = `
|
|
20
|
+
(call_expression
|
|
21
|
+
function: (identifier) @func
|
|
22
|
+
(#eq? @func "eval")
|
|
23
|
+
) @call
|
|
24
|
+
`;
|
|
25
|
+
|
|
26
|
+
const matches = parser.query(tree, query, lang);
|
|
27
|
+
|
|
28
|
+
for (const match of matches) {
|
|
29
|
+
// match.captures[0].node is the captured node
|
|
30
|
+
const node = match.captures[0].node;
|
|
31
|
+
|
|
32
|
+
// Extract the code line
|
|
33
|
+
const lineContent = content.split('\n')[node.startPosition.row] || '';
|
|
34
|
+
|
|
35
|
+
issues.push({
|
|
36
|
+
file: filePath,
|
|
37
|
+
line: node.startPosition.row + 1,
|
|
38
|
+
column: node.startPosition.column,
|
|
39
|
+
message: 'Dangerous use of eval() detected (AST Verified)',
|
|
40
|
+
type: 'Critical',
|
|
41
|
+
code: lineContent.trim()
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
} catch (error) {
|
|
46
|
+
// Fallback or ignore parse errors
|
|
47
|
+
// console.error(error);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return issues;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = { checkForEval };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const Parser = require('tree-sitter');
|
|
2
|
+
const JavaScript = require('tree-sitter-javascript');
|
|
3
|
+
const TypeScript = require('tree-sitter-typescript').typescript;
|
|
4
|
+
const TSX = require('tree-sitter-typescript').tsx;
|
|
5
|
+
|
|
6
|
+
class CodeParser {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.jsParser = new Parser();
|
|
9
|
+
this.jsParser.setLanguage(JavaScript);
|
|
10
|
+
|
|
11
|
+
this.tsParser = new Parser();
|
|
12
|
+
this.tsParser.setLanguage(TypeScript);
|
|
13
|
+
|
|
14
|
+
this.tsxParser = new Parser();
|
|
15
|
+
this.tsxParser.setLanguage(TSX);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Parse code into an AST
|
|
20
|
+
* @param {string} code
|
|
21
|
+
* @param {string} filePath
|
|
22
|
+
* @returns {Parser.Tree}
|
|
23
|
+
*/
|
|
24
|
+
parse(code, filePath) {
|
|
25
|
+
if (filePath.endsWith('.tsx')) {
|
|
26
|
+
return this.tsxParser.parse(code);
|
|
27
|
+
} else if (filePath.endsWith('.ts')) {
|
|
28
|
+
return this.tsParser.parse(code);
|
|
29
|
+
} else {
|
|
30
|
+
return this.jsParser.parse(code);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Run a query on the code
|
|
36
|
+
* @param {Parser.Tree} tree
|
|
37
|
+
* @param {string} queryStr
|
|
38
|
+
* @param {string} language - 'js', 'ts', 'tsx'
|
|
39
|
+
* @returns {Parser.QueryCapture[]}
|
|
40
|
+
*/
|
|
41
|
+
query(tree, queryStr, language = 'js') {
|
|
42
|
+
let langObj = JavaScript;
|
|
43
|
+
if (language === 'tsx') langObj = TSX;
|
|
44
|
+
if (language === 'ts') langObj = TypeScript;
|
|
45
|
+
|
|
46
|
+
const query = new Parser.Query(langObj, queryStr);
|
|
47
|
+
return query.matches(tree.rootNode);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = new CodeParser();
|
|
@@ -8,8 +8,9 @@ async function scanFolder(dir, fileList = []) {
|
|
|
8
8
|
const filePath = path.join(dir, file);
|
|
9
9
|
const stat = await fs.stat(filePath);
|
|
10
10
|
|
|
11
|
+
const ignoredDirs = ['node_modules', '.git', '.next', 'dist', 'build', 'out', 'coverage', '.vercel'];
|
|
11
12
|
if (stat.isDirectory()) {
|
|
12
|
-
if (file
|
|
13
|
+
if (!ignoredDirs.includes(file)) {
|
|
13
14
|
await scanFolder(filePath, fileList);
|
|
14
15
|
}
|
|
15
16
|
} else {
|