@indicated/vibeguard 1.5.2 → 1.7.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/src/mcp/server.ts CHANGED
@@ -245,6 +245,256 @@ function analyzeContext(finding: Finding, cwd: string): { signals: ContextSignal
245
245
  question = 'Is this a public key (anon/publishable) or an actual secret? Supabase anon keys are safe to expose.';
246
246
  break;
247
247
 
248
+ case 'insecure-randomness':
249
+ // Check if used for non-security purposes
250
+ if (finding.code.includes('color') || finding.code.includes('animation') ||
251
+ finding.code.includes('shuffle') || finding.code.includes('sample') ||
252
+ finding.code.includes('placeholder') || finding.code.includes('display')) {
253
+ signals.push({ signal: 'Appears to be used for non-security purposes (UI/display)', type: 'positive' });
254
+ confidence = 'low';
255
+ }
256
+ if (finding.code.includes('token') || finding.code.includes('secret') ||
257
+ finding.code.includes('password') || finding.code.includes('session') ||
258
+ finding.code.includes('nonce') || finding.code.includes('otp')) {
259
+ signals.push({ signal: 'Used for security-sensitive value generation', type: 'negative' });
260
+ confidence = 'high';
261
+ }
262
+ question = 'Is Math.random()/random used for security tokens or just UI/cosmetic purposes?';
263
+ break;
264
+
265
+ case 'weak-cryptography':
266
+ // Check if used for password hashing vs checksums
267
+ if (finding.code.includes('password') || finding.code.includes('secret') ||
268
+ finding.code.includes('token') || finding.code.includes('auth')) {
269
+ signals.push({ signal: 'Weak hash used for security-sensitive data', type: 'negative' });
270
+ confidence = 'high';
271
+ }
272
+ if (finding.code.includes('checksum') || finding.code.includes('etag') ||
273
+ finding.code.includes('cache') || finding.code.includes('fingerprint')) {
274
+ signals.push({ signal: 'May be used for non-security checksum/cache key', type: 'positive' });
275
+ confidence = 'low';
276
+ }
277
+ question = 'Is MD5/SHA1 used for security (bad) or for checksums/cache keys (acceptable)?';
278
+ break;
279
+
280
+ case 'nosql-injection':
281
+ // Check for input sanitization
282
+ if (fileContent.includes('mongo-sanitize') || fileContent.includes('express-mongo-sanitize') ||
283
+ fileContent.includes('sanitize') || fileContent.includes('validator')) {
284
+ signals.push({ signal: 'File uses input sanitization library', type: 'positive' });
285
+ confidence = 'low';
286
+ }
287
+ if (finding.code.includes('req.body') || finding.code.includes('req.query')) {
288
+ signals.push({ signal: 'User input passed directly to query', type: 'negative' });
289
+ confidence = 'high';
290
+ }
291
+ question = 'Is user input sanitized before being used in the NoSQL query?';
292
+ break;
293
+
294
+ case 'disabled-tls-verification':
295
+ if (fileContent.includes('development') || fileContent.includes('dev') ||
296
+ fileContent.includes('node_env') || fileContent.includes('test')) {
297
+ signals.push({ signal: 'May be conditionally enabled for development only', type: 'positive' });
298
+ confidence = 'medium';
299
+ } else {
300
+ signals.push({ signal: 'TLS verification unconditionally disabled', type: 'negative' });
301
+ confidence = 'high';
302
+ }
303
+ question = 'Is TLS verification only disabled in development, or also in production?';
304
+ break;
305
+
306
+ case 'unsafe-regex-construction':
307
+ if (fileContent.includes('escaperegex') || fileContent.includes('escape-string-regexp') ||
308
+ fileContent.includes('escaperegexp') || fileContent.includes('lodash') && fileContent.includes('escaperegexp')) {
309
+ signals.push({ signal: 'File imports regex escaping utility', type: 'positive' });
310
+ confidence = 'low';
311
+ }
312
+ if (finding.code.includes('req.') || finding.code.includes('query.') ||
313
+ finding.code.includes('params.') || finding.code.includes('body.')) {
314
+ signals.push({ signal: 'User input used directly in RegExp constructor', type: 'negative' });
315
+ confidence = 'high';
316
+ }
317
+ question = 'Is the user input escaped before being used in the RegExp constructor?';
318
+ break;
319
+
320
+ case 'postmessage-no-origin':
321
+ if (fileContent.includes('event.origin') || fileContent.includes('e.origin') ||
322
+ fileContent.includes('msg.origin')) {
323
+ signals.push({ signal: 'File checks origin elsewhere (may not be in handler)', type: 'positive' });
324
+ confidence = 'medium';
325
+ } else {
326
+ signals.push({ signal: 'No origin check found in file', type: 'negative' });
327
+ confidence = 'high';
328
+ }
329
+ question = 'Does the message event handler validate event.origin before processing data?';
330
+ break;
331
+
332
+ case 'hardcoded-db-credentials':
333
+ if (finding.code.includes('localhost') || finding.code.includes('127.0.0.1') ||
334
+ finding.code.includes('example.com') || finding.code.includes('test')) {
335
+ signals.push({ signal: 'Connection string points to localhost/test (likely development)', type: 'positive' });
336
+ confidence = 'low';
337
+ }
338
+ if (finding.code.includes('.env') || fileContent.includes('process.env') ||
339
+ fileContent.includes('os.environ')) {
340
+ signals.push({ signal: 'File also uses environment variables (may be fallback)', type: 'positive' });
341
+ confidence = 'medium';
342
+ }
343
+ question = 'Is this a development-only connection string, or does it contain production credentials?';
344
+ break;
345
+
346
+ case 'ssti-vulnerability':
347
+ signals.push({ signal: 'User-controlled template rendering is almost always dangerous', type: 'negative' });
348
+ confidence = 'high';
349
+ question = 'Is user input being rendered as a template? This is nearly always a critical vulnerability.';
350
+ break;
351
+
352
+ case 'timing-attack':
353
+ if (finding.code.includes('timingsafeequal') || finding.code.includes('compare_digest') ||
354
+ fileContent.includes('timingsafeequal') || fileContent.includes('compare_digest')) {
355
+ signals.push({ signal: 'File uses constant-time comparison elsewhere', type: 'positive' });
356
+ confidence = 'low';
357
+ }
358
+ if (finding.code.includes('===') && (finding.code.includes('token') || finding.code.includes('secret'))) {
359
+ signals.push({ signal: 'Direct === comparison of secret values', type: 'negative' });
360
+ confidence = 'medium';
361
+ }
362
+ question = 'Is this comparing secrets/tokens? If so, use constant-time comparison.';
363
+ break;
364
+
365
+ case 'electron-insecure-config':
366
+ if (finding.code.includes('nodeIntegration') && finding.code.includes('true')) {
367
+ signals.push({ signal: 'nodeIntegration enabled exposes Node.js APIs to web content', type: 'negative' });
368
+ confidence = 'high';
369
+ }
370
+ if (finding.code.includes('contextIsolation') && finding.code.includes('false')) {
371
+ signals.push({ signal: 'contextIsolation disabled allows prototype pollution from web content', type: 'negative' });
372
+ confidence = 'high';
373
+ }
374
+ question = 'Are these Electron security settings intentionally relaxed? This is dangerous for apps loading remote content.';
375
+ break;
376
+
377
+ case 'mass-assignment':
378
+ if (fileContent.includes('whitelist') || fileContent.includes('allowedfields') ||
379
+ fileContent.includes('pick') || fileContent.includes('only')) {
380
+ signals.push({ signal: 'File may use field whitelisting', type: 'positive' });
381
+ confidence = 'medium';
382
+ }
383
+ if (finding.code.includes('req.body') || finding.code.includes('request.data')) {
384
+ signals.push({ signal: 'Full request body passed to ORM operation', type: 'negative' });
385
+ confidence = 'high';
386
+ }
387
+ question = 'Is the user input filtered/whitelisted before being passed to the ORM create/update?';
388
+ break;
389
+
390
+ case 'jwt-missing-exp':
391
+ if (fileContent.includes('expiresIn') || fileContent.includes('exp:') || fileContent.includes("'exp'")) {
392
+ signals.push({ signal: 'File sets expiration elsewhere', type: 'positive' });
393
+ confidence = 'low';
394
+ } else {
395
+ signals.push({ signal: 'No expiration configuration found in file', type: 'negative' });
396
+ confidence = 'high';
397
+ }
398
+ question = 'Does the JWT payload include an exp claim, or is expiresIn set elsewhere?';
399
+ break;
400
+
401
+ case 'jwt-weak-secret':
402
+ if (finding.code.includes('process.env') || finding.code.includes('os.environ') ||
403
+ finding.code.includes('config.')) {
404
+ signals.push({ signal: 'Secret loaded from environment/config', type: 'positive' });
405
+ confidence = 'low';
406
+ }
407
+ if (/['"`][^'"`]{1,15}['"`]/.test(finding.code)) {
408
+ signals.push({ signal: 'Short hardcoded string used as signing secret', type: 'negative' });
409
+ confidence = 'high';
410
+ }
411
+ question = 'Is the JWT signing secret loaded from environment variables or hardcoded?';
412
+ break;
413
+
414
+ case 'csp-unsafe-inline':
415
+ if (fileContent.includes('nonce') || fileContent.includes('hash-')) {
416
+ signals.push({ signal: 'File uses nonce or hash-based CSP (may be transitioning)', type: 'positive' });
417
+ confidence = 'medium';
418
+ }
419
+ if (finding.code.includes('unsafe-eval')) {
420
+ signals.push({ signal: 'unsafe-eval allows arbitrary script execution', type: 'negative' });
421
+ confidence = 'high';
422
+ }
423
+ if (finding.code.includes('unsafe-inline')) {
424
+ signals.push({ signal: 'unsafe-inline defeats XSS protection from CSP', type: 'negative' });
425
+ confidence = 'high';
426
+ }
427
+ question = 'Is unsafe-inline/unsafe-eval required for third-party scripts, or can nonces/hashes be used instead?';
428
+ break;
429
+
430
+ case 'cors-credentials-wildcard':
431
+ signals.push({ signal: 'Wildcard origin with credentials allows any site to make authenticated requests', type: 'negative' });
432
+ confidence = 'high';
433
+ question = 'Should this API be accessible from any origin with credentials? Restrict to specific trusted origins.';
434
+ break;
435
+
436
+ case 'password-hash-weak':
437
+ if (fileContent.includes('bcrypt') || fileContent.includes('scrypt') || fileContent.includes('argon2')) {
438
+ signals.push({ signal: 'File also uses a proper KDF (may be migrating)', type: 'positive' });
439
+ confidence = 'medium';
440
+ } else {
441
+ signals.push({ signal: 'No proper password KDF found in file', type: 'negative' });
442
+ confidence = 'high';
443
+ }
444
+ question = 'Is this hashing passwords for storage? Use bcrypt/scrypt/argon2 instead of raw hash functions.';
445
+ break;
446
+
447
+ case 'password-plaintext-storage':
448
+ if (fileContent.includes('bcrypt') || fileContent.includes('hash') ||
449
+ fileContent.includes('scrypt') || fileContent.includes('argon2')) {
450
+ signals.push({ signal: 'File contains hashing logic (may be applied before this line)', type: 'positive' });
451
+ confidence = 'medium';
452
+ } else {
453
+ signals.push({ signal: 'No password hashing found in file', type: 'negative' });
454
+ confidence = 'high';
455
+ }
456
+ question = 'Is the password hashed before being stored? Check if bcrypt.hash() or similar is called upstream.';
457
+ break;
458
+
459
+ case 'zip-slip':
460
+ if (fileContent.includes('path.resolve') || fileContent.includes('path.normalize') ||
461
+ fileContent.includes('os.path.abspath') || fileContent.includes('startswith')) {
462
+ signals.push({ signal: 'File has path validation logic', type: 'positive' });
463
+ confidence = 'low';
464
+ } else {
465
+ signals.push({ signal: 'No path validation found before extraction', type: 'negative' });
466
+ confidence = 'high';
467
+ }
468
+ question = 'Are extracted file paths validated to prevent writing outside the target directory?';
469
+ break;
470
+
471
+ case 'http-client-no-timeout':
472
+ if (fileContent.includes('timeout') || fileContent.includes('AbortController') ||
473
+ fileContent.includes('signal')) {
474
+ signals.push({ signal: 'File configures timeout elsewhere (may be set globally)', type: 'positive' });
475
+ confidence = 'low';
476
+ } else {
477
+ signals.push({ signal: 'No timeout configuration found in file', type: 'negative' });
478
+ confidence = 'medium';
479
+ }
480
+ question = 'Is a timeout configured globally (e.g., via session defaults) or should one be added per-request?';
481
+ break;
482
+
483
+ case 's3-public-read':
484
+ if (relativePath.includes('test') || relativePath.includes('example') || relativePath.includes('sample')) {
485
+ signals.push({ signal: 'File appears to be test/example code', type: 'positive' });
486
+ confidence = 'low';
487
+ }
488
+ if (fileContent.includes('public') && (fileContent.includes('static') || fileContent.includes('assets') || fileContent.includes('cdn'))) {
489
+ signals.push({ signal: 'May be intentional public bucket for static assets/CDN', type: 'positive' });
490
+ confidence = 'medium';
491
+ } else {
492
+ signals.push({ signal: 'S3 bucket with public access policy', type: 'negative' });
493
+ confidence = 'high';
494
+ }
495
+ question = 'Is this S3 bucket intentionally public (static assets/CDN) or should access be restricted?';
496
+ break;
497
+
248
498
  default:
249
499
  question = `Verify if this ${finding.rule.name} finding is a real security issue in your specific context.`;
250
500
  }
@@ -34,6 +34,123 @@ const pythonPatterns: { ruleId: string; pattern: RegExp }[] = [
34
34
  ruleId: 'verbose-errors',
35
35
  pattern: /app\.run\s*\([^)]*debug\s*=\s*True/i,
36
36
  },
37
+ // Insecure randomness - Python specific
38
+ {
39
+ ruleId: 'insecure-randomness',
40
+ pattern: /(?:token|key|secret|session|nonce|salt|otp|password)\s*=\s*['"`]?.*?random\.(?:choice|randint|sample)\s*\(/i,
41
+ },
42
+ {
43
+ ruleId: 'insecure-randomness',
44
+ pattern: /random\.(?:random|randint|choice|getrandbits)\s*\(\s*\).*(?:token|key|secret|session|password|nonce)/i,
45
+ },
46
+ // Weak cryptography - Python hashlib
47
+ {
48
+ ruleId: 'weak-cryptography',
49
+ pattern: /hashlib\.(?:md5|sha1)\s*\(\s*(?:password|secret|token)/i,
50
+ },
51
+ // Python SSTI - render_template_string
52
+ {
53
+ ruleId: 'ssti-vulnerability',
54
+ pattern: /render_template_string\s*\(\s*(?:request\.(?:args|form|data|values)|f['"`])/,
55
+ },
56
+ // Python assert for security
57
+ {
58
+ ruleId: 'python-assert-security',
59
+ pattern: /^\s*assert\s+.*(?:is_admin|is_authenticated|is_staff|has_perm|permission|authorized)/m,
60
+ },
61
+ // Unsafe tempfile
62
+ {
63
+ ruleId: 'unsafe-tempfile',
64
+ pattern: /tempfile\.mktemp\s*\(/,
65
+ },
66
+ // Python timing attack
67
+ {
68
+ ruleId: 'timing-attack',
69
+ pattern: /(?:token|secret|password|api_key|signature)\s*==\s*(?:request\.|data\[|params\[)/i,
70
+ },
71
+ // Python NoSQL injection (PyMongo)
72
+ {
73
+ ruleId: 'nosql-injection',
74
+ pattern: /\.find(?:_one)?\s*\(\s*(?:request\.(?:json|data|form|args)|json\.loads)/,
75
+ },
76
+ // Python mass assignment
77
+ {
78
+ ruleId: 'mass-assignment',
79
+ pattern: /\.objects\.create\s*\(\s*\*\*request\.(?:data|POST)/,
80
+ },
81
+ // Python log injection
82
+ {
83
+ ruleId: 'log-injection',
84
+ pattern: /(?:logger|logging)\.(?:info|warn|warning|error|debug)\s*\(\s*f?['"`][^'"`]*\{(?:request\.|req\.|user_input|data\[)/,
85
+ },
86
+
87
+ // JWT missing expiration - Python (PyJWT)
88
+ {
89
+ ruleId: 'jwt-missing-exp',
90
+ pattern: /jwt\.encode\s*\(\s*\{(?![^}]*['"`]exp['"`])[^}]*\}\s*,/,
91
+ },
92
+ // JWT weak secret - Python
93
+ {
94
+ ruleId: 'jwt-weak-secret',
95
+ pattern: /jwt\.encode\s*\([^,]+,\s*['"`][^'"`]{1,15}['"`]/,
96
+ },
97
+ {
98
+ ruleId: 'jwt-weak-secret',
99
+ pattern: /jwt\.decode\s*\([^,]+,\s*['"`][^'"`]{1,15}['"`]/,
100
+ },
101
+
102
+ // CORS credentials wildcard - FastAPI/Flask
103
+ {
104
+ ruleId: 'cors-credentials-wildcard',
105
+ pattern: /allow_origins\s*=\s*\[\s*['"`]\*['"`]\s*\][\s\S]{0,100}allow_credentials\s*=\s*True/,
106
+ },
107
+ {
108
+ ruleId: 'cors-credentials-wildcard',
109
+ pattern: /allow_credentials\s*=\s*True[\s\S]{0,100}allow_origins\s*=\s*\[\s*['"`]\*['"`]\s*\]/,
110
+ },
111
+
112
+ // Password hash weak - Python hashlib for passwords
113
+ {
114
+ ruleId: 'password-hash-weak',
115
+ pattern: /hashlib\.(?:md5|sha1|sha256)\s*\(\s*(?:password|passwd|pass|pwd)/i,
116
+ },
117
+
118
+ // Password plaintext storage - Django/SQLAlchemy
119
+ {
120
+ ruleId: 'password-plaintext-storage',
121
+ pattern: /\.(?:create|create_user)\s*\([^)]*password\s*=\s*(?:request\.(?:data|POST)\[?['"`]?password|data\[['"`]password)/,
122
+ },
123
+ {
124
+ ruleId: 'password-plaintext-storage',
125
+ pattern: /\.password\s*=\s*(?:request\.(?:data|POST|form)\[?['"`]?password|data\[['"`]password)/,
126
+ },
127
+
128
+ // Zip slip - Python zipfile/tarfile
129
+ {
130
+ ruleId: 'zip-slip',
131
+ pattern: /(?:ZipFile|zipfile\.ZipFile)\s*\([^)]*\)\.extractall\s*\(/,
132
+ },
133
+ {
134
+ ruleId: 'zip-slip',
135
+ pattern: /tarfile\.open\s*\([^)]*\)\.extractall\s*\(/,
136
+ },
137
+
138
+ // HTTP client no timeout - Python requests
139
+ {
140
+ ruleId: 'http-client-no-timeout',
141
+ pattern: /requests\.(?:get|post|put|delete|patch|head)\s*\(\s*[^)]*\)(?<![^)]*timeout)/,
142
+ },
143
+ // urllib without timeout
144
+ {
145
+ ruleId: 'http-client-no-timeout',
146
+ pattern: /urllib\.request\.urlopen\s*\(\s*[^)]*\)(?<![^)]*timeout)/,
147
+ },
148
+
149
+ // S3 public read - boto3
150
+ {
151
+ ruleId: 's3-public-read',
152
+ pattern: /ACL\s*=\s*['"`]public-read(?:-write)?['"`]/,
153
+ },
37
154
  ];
38
155
 
39
156
  export function scanPythonWithPatterns(