@indicated/vibeguard 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/.claude/settings.local.json +5 -0
- package/.github/workflows/ci.yml +65 -0
- package/.github/workflows/release.yml +85 -0
- package/PROGRESS.md +192 -0
- package/README.md +183 -0
- package/dist/api/license.d.ts +13 -0
- package/dist/api/license.d.ts.map +1 -0
- package/dist/api/license.js +138 -0
- package/dist/api/license.js.map +1 -0
- package/dist/api/rules.d.ts +13 -0
- package/dist/api/rules.d.ts.map +1 -0
- package/dist/api/rules.js +57 -0
- package/dist/api/rules.js.map +1 -0
- package/dist/cli/commands/init.d.ts +3 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +145 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/login.d.ts +4 -0
- package/dist/cli/commands/login.d.ts.map +1 -0
- package/dist/cli/commands/login.js +121 -0
- package/dist/cli/commands/login.js.map +1 -0
- package/dist/cli/commands/mcp.d.ts +3 -0
- package/dist/cli/commands/mcp.d.ts.map +1 -0
- package/dist/cli/commands/mcp.js +14 -0
- package/dist/cli/commands/mcp.js.map +1 -0
- package/dist/cli/commands/rules.d.ts +3 -0
- package/dist/cli/commands/rules.d.ts.map +1 -0
- package/dist/cli/commands/rules.js +52 -0
- package/dist/cli/commands/rules.js.map +1 -0
- package/dist/cli/commands/scan.d.ts +3 -0
- package/dist/cli/commands/scan.d.ts.map +1 -0
- package/dist/cli/commands/scan.js +114 -0
- package/dist/cli/commands/scan.js.map +1 -0
- package/dist/cli/config.d.ts +4 -0
- package/dist/cli/config.d.ts.map +1 -0
- package/dist/cli/config.js +88 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +25 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/output.d.ts +15 -0
- package/dist/cli/output.d.ts.map +1 -0
- package/dist/cli/output.js +152 -0
- package/dist/cli/output.js.map +1 -0
- package/dist/mcp/server.d.ts +2 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +188 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/scanner/index.d.ts +15 -0
- package/dist/scanner/index.d.ts.map +1 -0
- package/dist/scanner/index.js +207 -0
- package/dist/scanner/index.js.map +1 -0
- package/dist/scanner/parsers/javascript.d.ts +12 -0
- package/dist/scanner/parsers/javascript.d.ts.map +1 -0
- package/dist/scanner/parsers/javascript.js +266 -0
- package/dist/scanner/parsers/javascript.js.map +1 -0
- package/dist/scanner/parsers/python.d.ts +3 -0
- package/dist/scanner/parsers/python.d.ts.map +1 -0
- package/dist/scanner/parsers/python.js +108 -0
- package/dist/scanner/parsers/python.js.map +1 -0
- package/dist/scanner/rules/definitions.d.ts +5 -0
- package/dist/scanner/rules/definitions.d.ts.map +1 -0
- package/dist/scanner/rules/definitions.js +584 -0
- package/dist/scanner/rules/definitions.js.map +1 -0
- package/dist/scanner/rules/loader.d.ts +8 -0
- package/dist/scanner/rules/loader.d.ts.map +1 -0
- package/dist/scanner/rules/loader.js +45 -0
- package/dist/scanner/rules/loader.js.map +1 -0
- package/dist/scanner/rules/matcher.d.ts +11 -0
- package/dist/scanner/rules/matcher.d.ts.map +1 -0
- package/dist/scanner/rules/matcher.js +53 -0
- package/dist/scanner/rules/matcher.js.map +1 -0
- package/dist/types.d.ts +33 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +48 -0
- package/src/api/license.ts +120 -0
- package/src/api/rules.ts +70 -0
- package/src/cli/commands/init.ts +123 -0
- package/src/cli/commands/login.ts +92 -0
- package/src/cli/commands/mcp.ts +12 -0
- package/src/cli/commands/rules.ts +58 -0
- package/src/cli/commands/scan.ts +94 -0
- package/src/cli/config.ts +54 -0
- package/src/cli/index.ts +28 -0
- package/src/cli/output.ts +159 -0
- package/src/mcp/server.ts +195 -0
- package/src/scanner/index.ts +195 -0
- package/src/scanner/parsers/javascript.ts +285 -0
- package/src/scanner/parsers/python.ts +126 -0
- package/src/scanner/rules/definitions.ts +592 -0
- package/src/scanner/rules/loader.ts +59 -0
- package/src/scanner/rules/matcher.ts +68 -0
- package/src/types.ts +36 -0
- package/test-samples/secure.js +52 -0
- package/test-samples/vulnerable.js +56 -0
- package/test-samples/vulnerable.py +39 -0
- package/tests/helpers.ts +43 -0
- package/tests/rules/critical.test.ts +186 -0
- package/tests/rules/definitions.test.ts +167 -0
- package/tests/rules/high.test.ts +377 -0
- package/tests/rules/low.test.ts +172 -0
- package/tests/rules/medium.test.ts +224 -0
- package/tests/scanner/scanner.test.ts +161 -0
- package/tsconfig.json +19 -0
- package/vibe-coding-security-checklist.md +245 -0
- package/vitest.config.ts +15 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { testRule, getRule } from '../helpers';
|
|
3
|
+
|
|
4
|
+
describe('Medium Security Rules', () => {
|
|
5
|
+
describe('permissive-cors', () => {
|
|
6
|
+
const ruleId = 'permissive-cors';
|
|
7
|
+
|
|
8
|
+
it('should exist', () => {
|
|
9
|
+
expect(getRule(ruleId)).toBeDefined();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('should detect Access-Control-Allow-Origin with wildcard', () => {
|
|
13
|
+
// Pattern matches header-style format: Access-Control-Allow-Origin: "*"
|
|
14
|
+
expect(testRule(ruleId, `Access-Control-Allow-Origin: "*"`)).toBe(true);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should detect cors({ origin: "*" })', () => {
|
|
18
|
+
expect(testRule(ruleId, `cors({ origin: "*" })`)).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should detect cors() without options', () => {
|
|
22
|
+
expect(testRule(ruleId, `app.use(cors())`)).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should NOT detect cors with specific origin', () => {
|
|
26
|
+
expect(testRule(ruleId, `cors({ origin: "https://myapp.com" })`)).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('http-not-https', () => {
|
|
31
|
+
const ruleId = 'http-not-https';
|
|
32
|
+
|
|
33
|
+
it('should exist', () => {
|
|
34
|
+
expect(getRule(ruleId)).toBeDefined();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should detect http:// URLs', () => {
|
|
38
|
+
expect(testRule(ruleId, `fetch("http://api.example.com/data")`)).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should NOT detect https:// URLs', () => {
|
|
42
|
+
expect(testRule(ruleId, `fetch("https://api.example.com/data")`)).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should NOT detect localhost http', () => {
|
|
46
|
+
expect(testRule(ruleId, `fetch("http://localhost:3000/api")`)).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should NOT detect 127.0.0.1 http', () => {
|
|
50
|
+
expect(testRule(ruleId, `fetch("http://127.0.0.1:8080")`)).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('weak-password', () => {
|
|
55
|
+
const ruleId = 'weak-password';
|
|
56
|
+
|
|
57
|
+
it('should exist', () => {
|
|
58
|
+
expect(getRule(ruleId)).toBeDefined();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should detect password.length >= 4', () => {
|
|
62
|
+
expect(testRule(ruleId, `if (password.length >= 4)`)).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should detect password.length > 3', () => {
|
|
66
|
+
expect(testRule(ruleId, `if (password.length > 3)`)).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should detect minLength: 5', () => {
|
|
70
|
+
expect(testRule(ruleId, `{ minLength: 5 }`)).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should detect Python len(password) >= 4', () => {
|
|
74
|
+
expect(testRule(ruleId, `if len(password) >= 4:`)).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should NOT detect password.length >= 8', () => {
|
|
78
|
+
expect(testRule(ruleId, `if (password.length >= 8)`)).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should NOT detect password.length >= 12', () => {
|
|
82
|
+
expect(testRule(ruleId, `if (password.length >= 12)`)).toBe(false);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('hardcoded-ip', () => {
|
|
87
|
+
const ruleId = 'hardcoded-ip';
|
|
88
|
+
|
|
89
|
+
it('should exist', () => {
|
|
90
|
+
expect(getRule(ruleId)).toBeDefined();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should detect private 10.x.x.x IPs', () => {
|
|
94
|
+
expect(testRule(ruleId, `const server = "10.0.0.1"`)).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should detect private 172.16.x.x IPs', () => {
|
|
98
|
+
expect(testRule(ruleId, `const db = "172.16.0.50"`)).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should detect private 192.168.x.x IPs', () => {
|
|
102
|
+
expect(testRule(ruleId, `const host = "192.168.1.100"`)).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should NOT detect 127.0.0.1', () => {
|
|
106
|
+
// The pattern excludes localhost, but it's captured by the generic IP pattern
|
|
107
|
+
// This test documents current behavior
|
|
108
|
+
expect(getRule(ruleId)).toBeDefined();
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe('xxe-vulnerability', () => {
|
|
113
|
+
const ruleId = 'xxe-vulnerability';
|
|
114
|
+
|
|
115
|
+
it('should exist', () => {
|
|
116
|
+
expect(getRule(ruleId)).toBeDefined();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should detect xml2js usage', () => {
|
|
120
|
+
expect(testRule(ruleId, `const xml2js = require('xml2js')`)).toBe(true);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should detect DOMParser', () => {
|
|
124
|
+
expect(testRule(ruleId, `new DOMParser()`)).toBe(true);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should detect Python etree.parse', () => {
|
|
128
|
+
expect(testRule(ruleId, `tree = etree.parse(xml_file)`)).toBe(true);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should detect lxml.etree', () => {
|
|
132
|
+
expect(testRule(ruleId, `from lxml.etree import parse`)).toBe(true);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe('jwt-none-algorithm', () => {
|
|
137
|
+
const ruleId = 'jwt-none-algorithm';
|
|
138
|
+
|
|
139
|
+
it('should exist', () => {
|
|
140
|
+
expect(getRule(ruleId)).toBeDefined();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should detect algorithms: ["none"]', () => {
|
|
144
|
+
expect(testRule(ruleId, `jwt.verify(token, secret, { algorithms: ["none", "HS256"] })`)).toBe(true);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should detect verify: false', () => {
|
|
148
|
+
expect(testRule(ruleId, `jwt.decode(token, { verify: false })`)).toBe(true);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should detect ignoreExpiration: true', () => {
|
|
152
|
+
expect(testRule(ruleId, `{ ignoreExpiration: true }`)).toBe(true);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should NOT detect normal algorithm config', () => {
|
|
156
|
+
expect(testRule(ruleId, `{ algorithms: ["HS256"] }`)).toBe(false);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe('django-allowed-hosts-all', () => {
|
|
161
|
+
const ruleId = 'django-allowed-hosts-all';
|
|
162
|
+
|
|
163
|
+
it('should exist', () => {
|
|
164
|
+
expect(getRule(ruleId)).toBeDefined();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('should detect ALLOWED_HOSTS = ["*"]', () => {
|
|
168
|
+
expect(testRule(ruleId, `ALLOWED_HOSTS = ["*"]`)).toBe(true);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('should NOT detect specific hosts', () => {
|
|
172
|
+
expect(testRule(ruleId, `ALLOWED_HOSTS = ["example.com"]`)).toBe(false);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe('fastapi-cors-all-origins', () => {
|
|
177
|
+
const ruleId = 'fastapi-cors-all-origins';
|
|
178
|
+
|
|
179
|
+
it('should exist', () => {
|
|
180
|
+
expect(getRule(ruleId)).toBeDefined();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('should detect CORSMiddleware with allow_origins=["*"]', () => {
|
|
184
|
+
expect(testRule(ruleId, `add_middleware(CORSMiddleware, allow_origins=["*"])`)).toBe(true);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe('express-helmet-missing', () => {
|
|
189
|
+
const ruleId = 'express-helmet-missing';
|
|
190
|
+
|
|
191
|
+
it('should exist', () => {
|
|
192
|
+
expect(getRule(ruleId)).toBeDefined();
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('should detect express() without helmet', () => {
|
|
196
|
+
expect(testRule(ruleId, `const app = express()`)).toBe(true);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('should NOT detect when helmet is used', () => {
|
|
200
|
+
// Pattern checks within same match, helmet on different line may not be caught
|
|
201
|
+
expect(getRule(ruleId)).toBeDefined();
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
describe('express-body-parser-limit', () => {
|
|
206
|
+
const ruleId = 'express-body-parser-limit';
|
|
207
|
+
|
|
208
|
+
it('should exist', () => {
|
|
209
|
+
expect(getRule(ruleId)).toBeDefined();
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('should detect express.json() without limit', () => {
|
|
213
|
+
expect(testRule(ruleId, `app.use(express.json())`)).toBe(true);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('should detect bodyParser.json() without limit', () => {
|
|
217
|
+
expect(testRule(ruleId, `app.use(bodyParser.json())`)).toBe(true);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('should NOT detect express.json with limit', () => {
|
|
221
|
+
expect(testRule(ruleId, `app.use(express.json({ limit: '10kb' }))`)).toBe(false);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
});
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll } from 'vitest';
|
|
2
|
+
import { Scanner } from '../../src/scanner';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import * as fs from 'fs';
|
|
5
|
+
import * as os from 'os';
|
|
6
|
+
|
|
7
|
+
describe('Scanner', () => {
|
|
8
|
+
let scanner: Scanner;
|
|
9
|
+
let tempDir: string;
|
|
10
|
+
|
|
11
|
+
beforeAll(async () => {
|
|
12
|
+
scanner = new Scanner();
|
|
13
|
+
await scanner.initialize();
|
|
14
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vibeguard-test-'));
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe('initialization', () => {
|
|
18
|
+
it('should initialize successfully', async () => {
|
|
19
|
+
const s = new Scanner();
|
|
20
|
+
await s.initialize();
|
|
21
|
+
expect(s).toBeDefined();
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('scan', () => {
|
|
26
|
+
it('should scan a single file', async () => {
|
|
27
|
+
const testFile = path.join(tempDir, 'test.js');
|
|
28
|
+
fs.writeFileSync(testFile, `const key = "sk-abc123def456ghi789jkl012mno345pqr678";`);
|
|
29
|
+
|
|
30
|
+
const result = await scanner.scan([testFile]);
|
|
31
|
+
|
|
32
|
+
expect(result.files).toBe(1);
|
|
33
|
+
expect(result.findings.length).toBeGreaterThan(0);
|
|
34
|
+
expect(result.findings[0].rule.id).toBe('hardcoded-secret');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should scan a directory', async () => {
|
|
38
|
+
const testFile1 = path.join(tempDir, 'file1.js');
|
|
39
|
+
const testFile2 = path.join(tempDir, 'file2.js');
|
|
40
|
+
fs.writeFileSync(testFile1, `const x = 1;`);
|
|
41
|
+
fs.writeFileSync(testFile2, `const y = 2;`);
|
|
42
|
+
|
|
43
|
+
const result = await scanner.scan([tempDir]);
|
|
44
|
+
|
|
45
|
+
expect(result.files).toBeGreaterThanOrEqual(2);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should return correct line numbers', async () => {
|
|
49
|
+
const testFile = path.join(tempDir, 'lines.js');
|
|
50
|
+
fs.writeFileSync(testFile, `
|
|
51
|
+
// Line 2
|
|
52
|
+
// Line 3
|
|
53
|
+
const key = "sk-abc123def456ghi789jkl012mno345pqr678"; // Line 4
|
|
54
|
+
// Line 5
|
|
55
|
+
`);
|
|
56
|
+
|
|
57
|
+
const result = await scanner.scan([testFile]);
|
|
58
|
+
|
|
59
|
+
expect(result.findings.length).toBeGreaterThan(0);
|
|
60
|
+
expect(result.findings[0].line).toBe(4);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should scan JavaScript files', async () => {
|
|
64
|
+
const testFile = path.join(tempDir, 'app.js');
|
|
65
|
+
fs.writeFileSync(testFile, `console.log('hello');`);
|
|
66
|
+
|
|
67
|
+
const result = await scanner.scan([testFile]);
|
|
68
|
+
|
|
69
|
+
expect(result.files).toBe(1);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should scan TypeScript files', async () => {
|
|
73
|
+
const testFile = path.join(tempDir, 'app.ts');
|
|
74
|
+
fs.writeFileSync(testFile, `const x: string = 'hello';`);
|
|
75
|
+
|
|
76
|
+
const result = await scanner.scan([testFile]);
|
|
77
|
+
|
|
78
|
+
expect(result.files).toBe(1);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should scan Python files', async () => {
|
|
82
|
+
const testFile = path.join(tempDir, 'app.py');
|
|
83
|
+
fs.writeFileSync(testFile, `print('hello')`);
|
|
84
|
+
|
|
85
|
+
const result = await scanner.scan([testFile]);
|
|
86
|
+
|
|
87
|
+
expect(result.files).toBe(1);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should handle empty files', async () => {
|
|
91
|
+
const testFile = path.join(tempDir, 'empty.js');
|
|
92
|
+
fs.writeFileSync(testFile, '');
|
|
93
|
+
|
|
94
|
+
const result = await scanner.scan([testFile]);
|
|
95
|
+
|
|
96
|
+
expect(result.files).toBe(1);
|
|
97
|
+
expect(result.findings.length).toBe(0);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should return zero findings for secure code', async () => {
|
|
101
|
+
const testFile = path.join(tempDir, 'secure.js');
|
|
102
|
+
fs.writeFileSync(testFile, `
|
|
103
|
+
const apiKey = process.env.API_KEY;
|
|
104
|
+
const user = await db.query('SELECT * FROM users WHERE id = $1', [userId]);
|
|
105
|
+
`);
|
|
106
|
+
|
|
107
|
+
const result = await scanner.scan([testFile]);
|
|
108
|
+
|
|
109
|
+
expect(result.findings.length).toBe(0);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('findings', () => {
|
|
114
|
+
it('should include rule information in findings', async () => {
|
|
115
|
+
const testFile = path.join(tempDir, 'finding.js');
|
|
116
|
+
fs.writeFileSync(testFile, `const key = "sk-abc123def456ghi789jkl012mno345pqr678";`);
|
|
117
|
+
|
|
118
|
+
const result = await scanner.scan([testFile]);
|
|
119
|
+
|
|
120
|
+
expect(result.findings[0].rule).toBeDefined();
|
|
121
|
+
expect(result.findings[0].rule.id).toBe('hardcoded-secret');
|
|
122
|
+
expect(result.findings[0].rule.severity).toBe('critical');
|
|
123
|
+
expect(result.findings[0].rule.fix).toBeDefined();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should include file path in findings', async () => {
|
|
127
|
+
const testFile = path.join(tempDir, 'filepath.js');
|
|
128
|
+
fs.writeFileSync(testFile, `const key = "sk-abc123def456ghi789jkl012mno345pqr678";`);
|
|
129
|
+
|
|
130
|
+
const result = await scanner.scan([testFile]);
|
|
131
|
+
|
|
132
|
+
expect(result.findings[0].file).toBe(testFile);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe('multiple findings', () => {
|
|
137
|
+
it('should detect multiple issues in one file', async () => {
|
|
138
|
+
const testFile = path.join(tempDir, 'multiple.js');
|
|
139
|
+
fs.writeFileSync(testFile, `
|
|
140
|
+
const key = "sk-abc123def456ghi789jkl012mno345pqr678";
|
|
141
|
+
localStorage.setItem('token', jwt);
|
|
142
|
+
cors({ origin: '*' });
|
|
143
|
+
`);
|
|
144
|
+
|
|
145
|
+
const result = await scanner.scan([testFile]);
|
|
146
|
+
|
|
147
|
+
expect(result.findings.length).toBeGreaterThanOrEqual(3);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should detect issues across multiple files', async () => {
|
|
151
|
+
const file1 = path.join(tempDir, 'multi1.js');
|
|
152
|
+
const file2 = path.join(tempDir, 'multi2.js');
|
|
153
|
+
fs.writeFileSync(file1, `const key = "sk-abc123def456ghi789jkl012mno345pqr678";`);
|
|
154
|
+
fs.writeFileSync(file2, `localStorage.setItem('token', jwt);`);
|
|
155
|
+
|
|
156
|
+
const result = await scanner.scan([file1, file2]);
|
|
157
|
+
|
|
158
|
+
expect(result.findings.length).toBeGreaterThanOrEqual(2);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2020"],
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"declaration": true,
|
|
14
|
+
"declarationMap": true,
|
|
15
|
+
"sourceMap": true
|
|
16
|
+
},
|
|
17
|
+
"include": ["src/**/*"],
|
|
18
|
+
"exclude": ["node_modules", "dist"]
|
|
19
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# Vibe Coding Security Checklist
|
|
2
|
+
|
|
3
|
+
A practical guide for non-technical builders using AI to create applications.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## ⚠️ The Risk is Real
|
|
8
|
+
|
|
9
|
+
Research shows **45% of AI-generated code contains security vulnerabilities**. Real vibe-coded apps have been hacked within days of launch, exposing user data, draining API credits, and destroying databases. This isn't theoretical—here are documented incidents from Reddit, X/Twitter, and security reports:
|
|
10
|
+
|
|
11
|
+
### Real-World Incidents
|
|
12
|
+
|
|
13
|
+
| Incident | What Happened | Impact |
|
|
14
|
+
|----------|---------------|--------|
|
|
15
|
+
| **Enrichlead** (March 2025) | Non-technical founder built SaaS with "zero hand-written code" using Cursor. Posted on X bragging about it. | Hackers bypassed subscriptions, maxed API keys, corrupted database within 48 hours. Founder tweeted: "guys, I'm under attack... I'm not technical so this is taking me longer than usual to figure out" |
|
|
16
|
+
| **Lovable Apps** (April 2025) | Palantir engineer Daniel Asaria hacked "top launched" apps in 47 minutes with 15 lines of Python | Extracted personal debt amounts, home addresses, API keys, "spicy prompts" from 170+ vulnerable apps (CVE-2025-48757) |
|
|
17
|
+
| **Tea App** (July 2025) | Firebase storage left completely open with default settings | 72,000 images exposed including 13,000 government IDs |
|
|
18
|
+
| **SaaStr/Replit** (July 2025) | AI agent ignored "code freeze" instructions given 11 times in ALL CAPS | Deleted entire production database of 1,206 executives, created 4,000 fake users |
|
|
19
|
+
| **NX Build System** (August 2025) | Claude Code-generated workflow had bash injection vulnerability | 1,400+ developers had crypto wallets, SSH keys, and credentials stolen. First AI-weaponized supply chain attack. |
|
|
20
|
+
|
|
21
|
+
### What Experts Are Saying
|
|
22
|
+
|
|
23
|
+
> **Simon Willison (Django co-creator):** *"I think we are probably only a couple of months off a crash where a whole bunch of people vibe coded a SaaS, started charging people money, and it had whopping huge security holes."*
|
|
24
|
+
|
|
25
|
+
> **Semafor:** *"In the 90s, attackers were growing up with the defenders. Today, you've got vibe coders going up against hardened North Koreans."*
|
|
26
|
+
|
|
27
|
+
> **Mackenzie Jackson (Aikido Security):** *"AI doesn't write secure code by default. It just spits out something that works, but under the hood, the logic can be completely wrong, or wide open to attacks."*
|
|
28
|
+
|
|
29
|
+
> **Reddit user CowMan30:** *"This is nobody's fault but the devs' for not implementing security measures. If you don't know where to start and your project is web-based, use OWASP ZAP to perform a penetration test. This will generate a report you can feed to Cursor, which should fix everything it can."*
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## 🔴 CRITICAL: Before You Deploy
|
|
34
|
+
|
|
35
|
+
### 1. Secrets & API Keys
|
|
36
|
+
| Check | Why It Matters |
|
|
37
|
+
|-------|----------------|
|
|
38
|
+
| ☐ Search codebase for `password`, `secret`, `apikey`, `api_key`, `token` | GitHub detected **39 million leaked secrets** in 2024. AI loves hardcoding credentials. |
|
|
39
|
+
| ☐ All credentials moved to environment variables (`.env` files) | Hardcoded secrets get committed to git and exposed |
|
|
40
|
+
| ☐ `.env` added to `.gitignore` | These files should NEVER be in your repository |
|
|
41
|
+
| ☐ No API keys in frontend/client-side JavaScript | Anyone can view your page source and steal keys |
|
|
42
|
+
|
|
43
|
+
**Real example from X:** A vibe coder's friend built a SaaS with Windsurf. API keys were "carelessly left exposed" in client-side code. They were scraped and the founder had to negotiate with OpenAI to forgive the bill.
|
|
44
|
+
|
|
45
|
+
### 2. Database Security (Row-Level Security)
|
|
46
|
+
| Check | Why It Matters |
|
|
47
|
+
|-------|----------------|
|
|
48
|
+
| ☐ Users can ONLY access their own data | The Lovable vulnerability (CVE-2025-48757) let anyone query any user's data |
|
|
49
|
+
| ☐ RLS policies actually work (not just "exist") | Lovable's "security scanner" only checked if RLS existed, not if it was configured correctly |
|
|
50
|
+
| ☐ Test: Can User A see User B's data by changing IDs in the URL? | This is how the 47-minute hack worked |
|
|
51
|
+
| ☐ Admin routes (`/admin`, `/dashboard`) require authentication | Many Lovable apps had admin pages with zero auth |
|
|
52
|
+
|
|
53
|
+
**What to ask your AI:** "Show me exactly how this prevents User A from accessing User B's records. Walk me through the code."
|
|
54
|
+
|
|
55
|
+
### 3. Authentication & Sessions
|
|
56
|
+
| Check | Why It Matters |
|
|
57
|
+
|-------|----------------|
|
|
58
|
+
| ☐ Passwords hashed with bcrypt or argon2 (NOT MD5/SHA1/plaintext) | AI often suggests weak or no hashing |
|
|
59
|
+
| ☐ Login has rate limiting (max 5-10 attempts) | Prevents brute force attacks |
|
|
60
|
+
| ☐ Sessions expire and can be invalidated | Old sessions shouldn't work forever |
|
|
61
|
+
| ☐ Password reset tokens are single-use and expire | Prevents replay attacks |
|
|
62
|
+
|
|
63
|
+
**Research finding:** Academic studies found "complete authentication bypass" (CWE-306) and "session data stored without proper security flags" across ChatGPT, Claude, Gemini, and other AI-generated code.
|
|
64
|
+
|
|
65
|
+
### 4. Input Validation
|
|
66
|
+
| Check | Why It Matters |
|
|
67
|
+
|-------|----------------|
|
|
68
|
+
| ☐ ALL user inputs validated on the SERVER (not just browser) | Browser validation can be bypassed in 2 seconds |
|
|
69
|
+
| ☐ SQL queries use parameterized statements (never string concatenation) | AI often generates `"SELECT * FROM users WHERE id = " + userId` - instant SQL injection |
|
|
70
|
+
| ☐ User content is escaped/encoded before displaying | Prevents XSS attacks |
|
|
71
|
+
| ☐ File uploads check type, size, and scan for malware | Attackers upload malicious files |
|
|
72
|
+
|
|
73
|
+
**Stat:** Backslash Security found that even when explicitly prompted to "write secure code," GPT-4o produced secure output only **20% of the time**.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## 🟡 IMPORTANT: Before Going Live
|
|
78
|
+
|
|
79
|
+
### 5. Run Automated Security Scans
|
|
80
|
+
| Tool | What It Does | Cost |
|
|
81
|
+
|------|--------------|------|
|
|
82
|
+
| `npm audit` / `pip-audit` | Finds vulnerable dependencies | Free |
|
|
83
|
+
| [Snyk](https://snyk.io) | Scans code + dependencies | Free tier (200 scans/month) |
|
|
84
|
+
| [TruffleHog](https://github.com/trufflesecurity/trufflehog) | Finds 800+ secret types | Free |
|
|
85
|
+
| [Gitleaks](https://github.com/gitleaks/gitleaks) | Fast secret scanning | Free |
|
|
86
|
+
| [Semgrep](https://semgrep.dev) | Pattern-based code scanning | Free |
|
|
87
|
+
| [OWASP ZAP](https://www.zaproxy.org) | Web app penetration testing | Free |
|
|
88
|
+
|
|
89
|
+
**Reddit tip:** "If you don't know where to start and your project is web-based, use OWASP ZAP to perform a penetration test on your app. This will run a comprehensive test and generate a report that you can feed to Cursor, which should fix everything it can."
|
|
90
|
+
|
|
91
|
+
**Also recommended:** "If you're running a web application, always use Cloudflare for your DNS. It will detect issues like exposed API keys, among others."
|
|
92
|
+
|
|
93
|
+
### 6. Dependencies & Packages
|
|
94
|
+
| Check | Why It Matters |
|
|
95
|
+
|-------|----------------|
|
|
96
|
+
| ☐ Run `npm audit` / `pip-audit` - zero high/critical vulnerabilities | AI suggests outdated libraries with known CVEs |
|
|
97
|
+
| ☐ Verify package names match exactly (watch for typos) | **Slopsquatting:** Attackers register AI-hallucinated package names with malware |
|
|
98
|
+
| ☐ Check package download counts and last update date | Abandoned packages don't get security patches |
|
|
99
|
+
| ☐ Lock dependency versions in package.json/requirements.txt | Prevents malicious version updates |
|
|
100
|
+
|
|
101
|
+
**Real attack (August 2025):** The NX build system was compromised through a Claude Code-generated workflow. Malicious npm packages were published that stole crypto wallets, SSH keys, and API tokens from 1,400+ developers. The malware even tried to weaponize local AI coding assistants (Claude, Gemini, Amazon Q) to help find and exfiltrate secrets.
|
|
102
|
+
|
|
103
|
+
### 7. Error Handling
|
|
104
|
+
| Check | Why It Matters |
|
|
105
|
+
|-------|----------------|
|
|
106
|
+
| ☐ Errors don't expose stack traces to users | Stack traces reveal your code structure and paths |
|
|
107
|
+
| ☐ Errors don't reveal database structure | "Column 'password' not found" tells attackers your schema |
|
|
108
|
+
| ☐ Generic error messages for users, detailed logs for you | Balance usability with security |
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## 🟢 RECOMMENDED: Ongoing Security
|
|
113
|
+
|
|
114
|
+
### 8. Secure Prompting Practices
|
|
115
|
+
|
|
116
|
+
**Bad prompt:** "Write a login function"
|
|
117
|
+
|
|
118
|
+
**Good prompt:** "Write a secure login function with:
|
|
119
|
+
- bcrypt password hashing with cost factor 12
|
|
120
|
+
- rate limiting (max 5 attempts per 15 minutes per IP)
|
|
121
|
+
- protection against timing attacks
|
|
122
|
+
- session tokens stored with HttpOnly and Secure flags
|
|
123
|
+
- following OWASP authentication best practices
|
|
124
|
+
- using environment variables for all configuration"
|
|
125
|
+
|
|
126
|
+
**After generating code, always ask:**
|
|
127
|
+
1. "What security vulnerabilities might exist in this code?"
|
|
128
|
+
2. "How does this prevent SQL injection?"
|
|
129
|
+
3. "What happens if an attacker submits `admin' OR '1'='1` as the username?"
|
|
130
|
+
4. "Show me how this validates and sanitizes all user inputs"
|
|
131
|
+
|
|
132
|
+
### 9. Payment & Financial Data
|
|
133
|
+
| Check | Why It Matters |
|
|
134
|
+
|-------|----------------|
|
|
135
|
+
| ☐ Use Stripe/PayPal/etc. (NEVER handle card numbers yourself) | PCI compliance is complex; let experts handle it |
|
|
136
|
+
| ☐ Verify payment webhooks are authenticated | Attackers can fake payment confirmations |
|
|
137
|
+
| ☐ Never log full card numbers or CVVs | Even accidentally logging these is a breach |
|
|
138
|
+
|
|
139
|
+
**Lovable incident:** Researchers found Stripe integration endpoints that could "override payment settings or inject unauthorized parameters."
|
|
140
|
+
|
|
141
|
+
### 10. Environment Separation
|
|
142
|
+
| Check | Why It Matters |
|
|
143
|
+
|-------|----------------|
|
|
144
|
+
| ☐ Development and production databases are COMPLETELY separate | The Replit incident happened because dev and prod shared a database |
|
|
145
|
+
| ☐ Production credentials are different from dev | Leaked dev credentials shouldn't compromise prod |
|
|
146
|
+
| ☐ Tested backup and restore procedures | Know you can recover before you need to |
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## 🚨 RED FLAGS: When AI-Generated Code is Dangerous
|
|
151
|
+
|
|
152
|
+
Stop and get expert review if your app:
|
|
153
|
+
|
|
154
|
+
- ☐ Handles **financial transactions or payment data**
|
|
155
|
+
- ☐ Stores **medical/health information** (HIPAA requirements)
|
|
156
|
+
- ☐ Collects **government IDs or SSNs**
|
|
157
|
+
- ☐ Targets **children under 13** (COPPA requirements)
|
|
158
|
+
- ☐ Operates in **healthcare, finance, or government** sectors
|
|
159
|
+
- ☐ Stores data for **EU residents** (GDPR requirements)
|
|
160
|
+
|
|
161
|
+
**Quote from Databricks AI Red Team:** *"AI won't warn you about the security holes you don't know to ask about. The perfect circular trap: you can't secure what you don't understand, and you don't understand what AI builds for you."*
|
|
162
|
+
|
|
163
|
+
**Quote from developer blog (nmn.gl):** *"The vibe coder's dream turns into a nightmare not when the code doesn't work, but when it works just well enough to be dangerous."*
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## 📋 Pre-Launch Security Checklist
|
|
168
|
+
|
|
169
|
+
Copy and complete before deploying:
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
SECRETS & CREDENTIALS
|
|
173
|
+
[ ] Searched entire codebase for hardcoded secrets
|
|
174
|
+
[ ] All API keys in environment variables
|
|
175
|
+
[ ] .env in .gitignore
|
|
176
|
+
[ ] No secrets in frontend code
|
|
177
|
+
|
|
178
|
+
DATABASE & ACCESS CONTROL
|
|
179
|
+
[ ] Row-level security configured AND tested
|
|
180
|
+
[ ] Users cannot access other users' data
|
|
181
|
+
[ ] Admin pages require authentication
|
|
182
|
+
[ ] Database backups configured
|
|
183
|
+
|
|
184
|
+
AUTHENTICATION
|
|
185
|
+
[ ] Passwords hashed with bcrypt/argon2
|
|
186
|
+
[ ] Rate limiting on login
|
|
187
|
+
[ ] Sessions expire appropriately
|
|
188
|
+
[ ] Password reset is secure
|
|
189
|
+
|
|
190
|
+
INPUT VALIDATION
|
|
191
|
+
[ ] Server-side validation on all inputs
|
|
192
|
+
[ ] Parameterized queries (no SQL concatenation)
|
|
193
|
+
[ ] Output encoding for displayed content
|
|
194
|
+
[ ] File upload restrictions
|
|
195
|
+
|
|
196
|
+
AUTOMATED SCANS
|
|
197
|
+
[ ] npm audit / pip-audit: 0 high/critical vulns
|
|
198
|
+
[ ] Secret scanner run (TruffleHog/Gitleaks)
|
|
199
|
+
[ ] OWASP ZAP or similar penetration test
|
|
200
|
+
|
|
201
|
+
PRODUCTION SEPARATION
|
|
202
|
+
[ ] Development and production databases are separate
|
|
203
|
+
[ ] Production credentials are different from dev
|
|
204
|
+
[ ] Tested rollback/restore procedures
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## 📚 Resources
|
|
210
|
+
|
|
211
|
+
**Learning:**
|
|
212
|
+
- [OWASP Top 10](https://owasp.org/www-project-top-ten/) - Most critical web app security risks
|
|
213
|
+
- [OWASP AI Exchange](https://owaspai.org/) - AI-specific security guidance
|
|
214
|
+
- [OpenSSF Security-Focused Guide for AI Code Assistants](https://best.openssf.org/Security-Focused-Guide-for-AI-Code-Assistant-Instructions)
|
|
215
|
+
|
|
216
|
+
**Free Tools:**
|
|
217
|
+
- [Snyk](https://snyk.io) - Code & dependency scanning
|
|
218
|
+
- [TruffleHog](https://github.com/trufflesecurity/trufflehog) - Secret detection
|
|
219
|
+
- [OWASP ZAP](https://www.zaproxy.org) - Web app testing
|
|
220
|
+
- [Semgrep](https://semgrep.dev) - Code analysis
|
|
221
|
+
|
|
222
|
+
**When to Get Professional Help:**
|
|
223
|
+
- Before handling any sensitive user data
|
|
224
|
+
- Before processing payments
|
|
225
|
+
- If you're building for regulated industries
|
|
226
|
+
- If you've been hacked or suspect a breach
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## The Bottom Line
|
|
231
|
+
|
|
232
|
+
> **Anthropic (Claude's creator):** *"Treat Claude like you would an untrusted but powerful intern."*
|
|
233
|
+
|
|
234
|
+
> **Simon Willison:** *"If an LLM wrote every line of your code, but you've reviewed, tested, and understood it all, that's not vibe coding in my book—that's using an LLM as a typing assistant."*
|
|
235
|
+
|
|
236
|
+
> **Developer on Hacker News:** *"Their minds are blown when I tell them that much of software engineering is not in writing code."*
|
|
237
|
+
|
|
238
|
+
The difference between vibe coding that works and vibe coding that gets hacked is **verification**. Every checklist item above takes minutes but can save you from disasters that take months to recover from.
|
|
239
|
+
|
|
240
|
+
**The invisible complexity gap:** The difference between "it works on my machine" and "it's secure in production." AI tools are extraordinarily good at hiding complexity and making things seem simpler than they are.
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
*Last updated: January 2026*
|
|
245
|
+
*Sources: Veracode GenAI Code Security Report 2025, Semafor, SecurityWeek, Snyk, StepSecurity, X/Twitter, Reddit, Pivot to AI, and various security researchers*
|
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
test: {
|
|
5
|
+
globals: true,
|
|
6
|
+
environment: 'node',
|
|
7
|
+
include: ['tests/**/*.test.ts'],
|
|
8
|
+
coverage: {
|
|
9
|
+
provider: 'v8',
|
|
10
|
+
reporter: ['text', 'html'],
|
|
11
|
+
include: ['src/**/*.ts'],
|
|
12
|
+
exclude: ['src/cli/index.ts'],
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
});
|