guardvibe 2.9.5 → 2.9.7

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/build/index.js CHANGED
@@ -133,10 +133,14 @@ server.tool("check_dependencies", "Check npm, PyPI, or Go packages for known sec
133
133
  return val;
134
134
  }, z.array(packageSchema)).describe("List of packages to check: [{name, version, ecosystem}]"),
135
135
  }, async ({ packages }) => {
136
- const results = await checkDependencies(packages);
137
- return {
138
- content: [{ type: "text", text: results }],
139
- };
136
+ try {
137
+ const results = await checkDependencies(packages);
138
+ return { content: [{ type: "text", text: results }] };
139
+ }
140
+ catch (err) {
141
+ const msg = err instanceof Error ? err.message : String(err);
142
+ return { content: [{ type: "text", text: `⚠️ Dependency check failed: ${msg}\n\nThis may be a network issue reaching the OSV database. Try again or check your internet connection.` }] };
143
+ }
140
144
  });
141
145
  // Tool 5: Scan directory for security vulnerabilities (filesystem-native)
142
146
  server.tool("scan_directory", "Scan an entire project directory for security vulnerabilities. Reads files directly from the filesystem — no need to pass file contents. Returns a security score (A-F) and detailed findings. Includes scan metadata (ID, timestamp, duration, file hashes) for audit trails. Use baseline to compare with a previous scan.", {
@@ -170,7 +174,7 @@ server.tool("scan_directory", "Scan an entire project directory for security vul
170
174
  return { content: [{ type: "text", text: results + summary }] };
171
175
  });
172
176
  // Tool 6: Scan manifest/lockfile for dependency vulnerabilities
173
- server.tool("scan_dependencies", "Parse a lockfile or manifest (package.json, package-lock.json, requirements.txt, go.mod) and check all dependencies for known CVEs via the OSV database. Reads the file directly.", {
177
+ server.tool("scan_dependencies", "Parse a lockfile or manifest (package.json, package-lock.json, requirements.txt, go.mod) and check all dependencies for known CVEs via the OSV database. Reads the file directly. Use this after installing dependencies, during CI, or when auditing existing projects for vulnerable packages.", {
174
178
  manifest_path: z.string().describe("Path to manifest file (e.g. 'package.json', 'requirements.txt', 'go.mod')"),
175
179
  format: z.enum(["markdown", "json"]).default("markdown").describe("Output format: markdown (human) or json (machine-readable for agents)"),
176
180
  }, async ({ manifest_path, format }) => {
@@ -258,8 +262,14 @@ server.tool("check_package_health", "Check npm packages for typosquat risk, main
258
262
  packages: z.array(z.string()).describe("List of package names to check (e.g. ['lodash', 'expres', 'react-qeury'])"),
259
263
  format: z.enum(["markdown", "json"]).default("markdown").describe("Output format: markdown (human) or json (machine-readable for agents)"),
260
264
  }, async ({ packages, format }) => {
261
- const results = await checkPackageHealth(packages, format);
262
- return { content: [{ type: "text", text: results }] };
265
+ try {
266
+ const results = await checkPackageHealth(packages, format);
267
+ return { content: [{ type: "text", text: results }] };
268
+ }
269
+ catch (err) {
270
+ const msg = err instanceof Error ? err.message : String(err);
271
+ return { content: [{ type: "text", text: `⚠️ Package health check failed: ${msg}\n\nThis may be a network issue reaching the npm registry. Try again or check your internet connection.` }] };
272
+ }
263
273
  });
264
274
  // Tool 12: Auto-fix security vulnerabilities
265
275
  server.tool("fix_code", "Analyze code for security vulnerabilities and return fix suggestions with concrete patches. The AI agent can apply these patches to automatically fix issues. Returns structured fix data including before/after code, severity, and line numbers.", {
@@ -325,7 +335,7 @@ server.tool("scan_secrets_history", "Scan git history for leaked secrets. Finds
325
335
  return { content: [{ type: "text", text: results }] };
326
336
  });
327
337
  // Tool 17: Compliance Policy Check
328
- server.tool("policy_check", "Check project against compliance policies defined in .guardviberc. Validates custom framework requirements, severity thresholds, required controls, and risk exceptions. Returns pass/fail status with detailed findings per control.", {
338
+ server.tool("policy_check", "Check project against compliance policies defined in .guardviberc. Use this in CI/CD pipelines to enforce security gates, or before releases to verify compliance requirements are met. Validates custom framework requirements, severity thresholds, required controls, and risk exceptions. Returns pass/fail status with detailed findings per control.", {
329
339
  path: z.string().describe("Project root directory"),
330
340
  format: z.enum(["markdown", "json"]).default("markdown").describe("Output format"),
331
341
  }, async ({ path, format }) => {
@@ -173,6 +173,13 @@ function hasRoleCheckPattern(code) {
173
173
  // Destructured role check: const { role } = ...; if (role !== "admin")
174
174
  if (/\brole\b[\s\S]{0,100}?(?:!==|===)\s*["']/i.test(code))
175
175
  return true;
176
+ // Import + call of admin/role guard function (requireAdmin, requireRole, checkAdmin, etc.)
177
+ if (/import\s+.*(?:requireAdmin|requireRole|checkAdmin|isAdmin|verifyAdmin|assertAdmin)\b/i.test(code) &&
178
+ /(?:requireAdmin|requireRole|checkAdmin|isAdmin|verifyAdmin|assertAdmin)\s*\(/i.test(code))
179
+ return true;
180
+ // await requireAdmin() with error check pattern (naming-agnostic admin guard)
181
+ if (/await\s+(?:\w+\.)*\w*(?:Admin|admin)\w*\s*\([^)]*\)\s*;?\s*\n\s*if\s*\(/i.test(code))
182
+ return true;
176
183
  return false;
177
184
  }
178
185
  /**
@@ -202,6 +209,9 @@ const LEGITIMATE_PREFIXED_PACKAGES = new Set([
202
209
  "original-url", "original-fs",
203
210
  "secure-json-parse",
204
211
  "native-run",
212
+ "fast-sha256", "fast-text-encoding",
213
+ "svix",
214
+ "cheerio",
205
215
  ]);
206
216
  function isLegitimatePackage(name) {
207
217
  return LEGITIMATE_PREFIXED_PACKAGES.has(name);
@@ -258,6 +268,10 @@ export function analyzeCode(code, language, framework, filePath, configDir, rule
258
268
  const codeHasRedirectValidation = /(?:sanitize|validate|verify|check|safe|allowed)(?:Redirect|RedirectUrl|CallbackUrl)\s*\(/i.test(code) ||
259
269
  /import\s+.*(?:sanitizeRedirect|validateRedirect|safeRedirect)/i.test(code);
260
270
  const isMigrationFile = filePath ? /(?:migrations?|supabase\/migrations|seeds?|fixtures)\//i.test(filePath) : false;
271
+ const isSqlSchemaFile = filePath ? /(?:schema|migration|seed|ddl|init).*\.sql$/i.test(filePath) : false;
272
+ const isReactNative = /(?:react-native|from\s+['"]react-native['"]|from\s+['"]expo|import\s+.*\bexpo\b)/i.test(code);
273
+ const codeHasTimingSafeEqual = /(?:timingSafeEqual|timing.?safe|constant.?time)/i.test(code);
274
+ const codeHasFilenameSanitization = /(?:\.replace\s*\(\s*\/\[?\^?[a-z0-9\\-_\]]*\]?\/?[gi]*\s*,|sanitize(?:File|Name|Path)|safeName|cleanName)/i.test(code);
261
275
  const isPeerDeps = /["']peerDependencies["']/i.test(code);
262
276
  // Config: check custom auth function names from .guardviberc
263
277
  if (!codeHasAuthGuard && config.authFunctions && config.authFunctions.length > 0) {
@@ -291,6 +305,10 @@ export function analyzeCode(code, language, framework, filePath, configDir, rule
291
305
  // Skip auth rules when code has any auth guard pattern (naming-agnostic)
292
306
  if (codeHasAuthGuard && authRuleIds.has(rule.id))
293
307
  continue;
308
+ // Skip auth rules for intentionally public endpoints
309
+ // /api/public/*, health checks, config endpoints, recache/warmup
310
+ if (authRuleIds.has(rule.id) && filePath && /(?:\/api\/public\/|\/health|\/config|\/recache|\/warmup|\/ping|\/status)/i.test(filePath))
311
+ continue;
294
312
  // Skip admin role rules when code has any role/permission check
295
313
  if (codeHasRoleCheck && adminRoleRuleIds.has(rule.id))
296
314
  continue;
@@ -312,33 +330,131 @@ export function analyzeCode(code, language, framework, filePath, configDir, rule
312
330
  // Skip destructive DDL rules (VG540-VG542) and view rules (VG439) in migration directories
313
331
  if ((rule.id.startsWith("VG54") || rule.id === "VG439") && isMigrationFile)
314
332
  continue;
333
+ // Skip SQL injection rules in schema/migration .sql files (DDL, not user input)
334
+ if (rule.id === "VG543" && (isMigrationFile || isSqlSchemaFile))
335
+ continue;
336
+ // Skip web-only rules in React Native/mobile context
337
+ // VG427 (getSession) is correct in mobile; VG678/VG977/VG978 are HTTP-only concerns
338
+ if (isReactNative && ["VG427", "VG678", "VG977", "VG978", "VG964"].includes(rule.id))
339
+ continue;
315
340
  // Skip innerHTML/XSS rules when DOMPurify or sanitization is present
316
- if (codeHasSanitization && ["VG408", "VG012", "VG042"].includes(rule.id))
341
+ if (codeHasSanitization && ["VG408", "VG012", "VG042", "VG852"].includes(rule.id))
317
342
  continue;
343
+ // Skip innerHTML rules for static content patterns (JSON-LD structured data, theme scripts)
344
+ // These use hardcoded app-defined data, not user input
345
+ if (["VG408", "VG012", "VG042", "VG852"].includes(rule.id)) {
346
+ const hasStaticContent = /(?:JSON\.stringify|themeScript|structuredData|jsonLd|schema|gtag|analytics)/i.test(code);
347
+ const hasUserContent = /(?:userInput|userData|postContent|messageBody|commentHtml)/i.test(code);
348
+ if (hasStaticContent && !hasUserContent)
349
+ continue;
350
+ }
318
351
  // Skip SSRF rules when URL validation/allowlist pattern is present
319
- if (codeHasUrlValidation && ["VG120"].includes(rule.id))
352
+ if (codeHasUrlValidation && rule.id === "VG120")
353
+ continue;
354
+ // Skip SSRF for fetch() calls that only use relative URLs or known-safe patterns
355
+ // (internal API calls like /api/..., Supabase signed URLs)
356
+ if (rule.id === "VG120") {
357
+ const fetchCalls = code.match(/fetch\s*\(\s*(?:["'`]|url|signedUrl)/gi) || [];
358
+ const hasUserUrl = /fetch\s*\(\s*(?:(?:req|request|params|query|body|input|data)\s*[\[.]|new\s+URL\s*\(\s*(?:req|request))/i.test(code);
359
+ const onlyInternalFetches = !hasUserUrl &&
360
+ !/fetch\s*\(\s*["'`]https?:\/\//i.test(code) ||
361
+ /fetch\s*\(\s*(?:["'`]\/api\/|signedUrl|presignedUrl|uploadUrl)/i.test(code);
362
+ if (fetchCalls.length > 0 && onlyInternalFetches)
363
+ continue;
364
+ }
365
+ // Skip filename rules when UUID-based filename generation OR filename sanitization is present
366
+ if ((codeHasUuidFilename || codeHasFilenameSanitization) && rule.id === "VG993")
320
367
  continue;
321
- // Skip filename rules when UUID-based filename generation is present
322
- if (codeHasUuidFilename && rule.id === "VG993")
368
+ // Skip timing-unsafe comparison rule when timingSafeEqual is already used in the file
369
+ if (codeHasTimingSafeEqual && rule.id === "VG106")
323
370
  continue;
371
+ // Downgrade VG106 for non-secret variable names (TokenCount, tokenLength, etc.)
372
+ // These contain "token" but aren't actual secret comparisons
373
+ if (rule.id === "VG106") {
374
+ // Will be checked at match level below
375
+ }
324
376
  // Skip cron secret rules when custom verification function is present
325
377
  if (codeHasCronVerification && ["VG968", "VG503"].includes(rule.id))
326
378
  continue;
327
379
  // Skip open redirect rules when redirect URL validation is present
328
380
  if (codeHasRedirectValidation && ["VG425", "VG409", "VG660"].includes(rule.id))
329
381
  continue;
330
- // Skip VG131 (state-changing GET) when only read operations are present
331
- if (rule.id === "VG131") {
332
- // If code only has read operations (findMany, findFirst, count, aggregate, select)
333
- // and no actual mutations, skip this rule
334
- const hasMutation = /(?:\.create\s*\(|\.update\s*\(|\.delete\s*\(|\.destroy\s*\(|\.remove\s*\(|\.insert\s*\(|DELETE\s+FROM|UPDATE\s+\w|INSERT\s+INTO)/i.test(code);
335
- const onlyInComments = !hasMutation;
336
- if (onlyInComments)
382
+ // Skip VG105 JWT alg:none when code uses HMAC/custom token verification (not JWT library)
383
+ if (rule.id === "VG105") {
384
+ const usesJwtLib = /(?:jsonwebtoken|jose|jwt\.verify|jwt\.sign|jwt\.decode)\b/i.test(code);
385
+ const usesHmac = /(?:createHmac|hmac|crypto\.subtle\.sign|crypto\.sign)/i.test(code);
386
+ const importsCustomTokenLib = /import\s+.*(?:verifyToken|validateToken|checkToken|decodeToken)\b.*from\s+["'].*(?:token|auth|hmac)/i.test(code);
387
+ if (!usesJwtLib && (usesHmac || importsCustomTokenLib))
337
388
  continue;
338
389
  }
390
+ // Skip VG989 (X-Forwarded-For rate limit bypass) when project uses Next.js/Vercel
391
+ // Vercel sets X-Forwarded-For from its edge network — it's trustworthy in that context
392
+ if (rule.id === "VG989" && filePath && /(?:\/app\/|\/pages\/|next)/i.test(filePath))
393
+ continue;
394
+ // Skip VG678 (Missing X-Content-Type-Options) when not actually serving files:
395
+ // - Client components can't set HTTP response headers
396
+ // - Supabase getPublicUrl/getSignedUrl just generate URL strings
397
+ // - Email sending (Resend, nodemailer) doesn't serve files
398
+ // - React Native components are not HTTP endpoints
399
+ if (rule.id === "VG678") {
400
+ const isClientComponent = /^['"]use client['"]/.test(code.trimStart()) ||
401
+ (filePath && /Client\.\w+$/.test(filePath));
402
+ if (isClientComponent)
403
+ continue;
404
+ if (isReactNative)
405
+ continue;
406
+ const isEmailFile = /(?:resend|nodemailer|sendEmail|sendMail|email\.send)/i.test(code);
407
+ if (isEmailFile)
408
+ continue;
409
+ const hasOnlyUrlGeneration = /(?:getPublicUrl|getSignedUrl)\s*\(/i.test(code) &&
410
+ !/(?:createReadStream|sendFile|res\.download|\.pipe\s*\()/i.test(code);
411
+ if (hasOnlyUrlGeneration)
412
+ continue;
413
+ }
414
+ // Skip VG131 (state-changing GET) when the GET function body has no mutations.
415
+ // Must check per-GET-function, not the whole file — files with GET+POST would false-positive.
416
+ if (rule.id === "VG131") {
417
+ const getMatch = /export\s+(?:async\s+)?function\s+GET\s*\([^)]*\)\s*\{/.exec(code);
418
+ if (getMatch) {
419
+ const getStart = getMatch.index + getMatch[0].length;
420
+ let depth = 1, pos = getStart;
421
+ while (depth > 0 && pos < code.length) {
422
+ if (code[pos] === "{")
423
+ depth++;
424
+ else if (code[pos] === "}")
425
+ depth--;
426
+ pos++;
427
+ }
428
+ const getBody = code.substring(getStart, pos);
429
+ const hasMutationInGet = /(?:\.create\s*\(|\.update\s*\(|\.delete\s*\(|\.destroy\s*\(|\.remove\s*\(|\.insert\s*\(|\.upsert\s*\(|DELETE\s+FROM|UPDATE\s+\w|INSERT\s+INTO)/i.test(getBody);
430
+ if (!hasMutationInGet)
431
+ continue;
432
+ }
433
+ }
339
434
  // Skip CVE version rules in peerDependencies (ranges, not actual versions)
340
435
  if (isPeerDeps && rule.id === "VG903")
341
436
  continue;
437
+ // Skip VG140 (XXE) when file doesn't actually parse XML — just has XML-related imports
438
+ if (rule.id === "VG140") {
439
+ const hasXmlParsing = /(?:parseString|parseXml|xml2js|DOMParser|XMLParser)\s*\(/i.test(code);
440
+ if (!hasXmlParsing)
441
+ continue;
442
+ }
443
+ // Skip VG020 (wildcard dependency version) in lock files — engine constraints
444
+ // like "node": ">=6" are not dependency versions
445
+ if (rule.id === "VG020" && filePath && /(?:package-lock\.json|yarn\.lock|pnpm-lock\.yaml|npm-shrinkwrap\.json)$/.test(filePath))
446
+ continue;
447
+ // Skip VG430 (Supabase anon key on server) when file properly separates client/server
448
+ // Pattern: file exports both a client (anon key) and server (service_role) function
449
+ if (rule.id === "VG430") {
450
+ const hasServiceRole = /(?:SUPABASE_SERVICE_ROLE|service_role|serviceRole)/i.test(code);
451
+ const hasClientServer = /(?:createClient|createServerClient|createBrowserClient)/i.test(code) && hasServiceRole;
452
+ if (hasClientServer)
453
+ continue;
454
+ }
455
+ // Skip VG448 (Supabase RPC bypass RLS) when using service_role key (server-side)
456
+ if (rule.id === "VG448" && /(?:SUPABASE_SERVICE_ROLE|service_role|createServerSupabaseClient|createServerClient)/i.test(code))
457
+ continue;
342
458
  // VG872/VG873 legitimate package filtering is handled at match level below
343
459
  // Skip server-only import rule (VG964) for files that are inherently server-only:
344
460
  // Route Handlers (app/api/), middleware, instrumentation, next.config,
@@ -403,6 +519,12 @@ export function analyzeCode(code, language, framework, filePath, configDir, rule
403
519
  if (pkgMatch && isLegitimatePackage(pkgMatch[1]))
404
520
  continue;
405
521
  }
522
+ // Skip VG106 for non-secret variable names (TokenCount, tokenBalance, hashMap, etc.)
523
+ if (rule.id === "VG106") {
524
+ const varName = match[0].split(/\s*(?:===|!==|==|!=)/)[0].trim();
525
+ if (/(?:Count|Length|Balance|Map|List|Array|Index|Size|Total|Num|Id|Type|Name|Status|Data|Info|Error|Result|Response|Config|Option|Url|Path|Provider|Model|Limit|Quota|Rate|Max|Min)/i.test(varName))
526
+ continue;
527
+ }
406
528
  // Skip VG903 React version in peerDependencies sections
407
529
  if (rule.id === "VG903") {
408
530
  const beforeText = code.substring(0, match.index);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "guardvibe",
3
- "version": "2.9.5",
3
+ "version": "2.9.7",
4
4
  "mcpName": "io.github.goklab/guardvibe",
5
5
  "description": "Security MCP for vibe coding. 334 rules, 31 tools, CLI + doctor. Host security: CVE-2025-59536 hook injection, CVE-2026-21852 base URL hijack, MCP config audit, AI host hardening. Plus Next.js, Supabase, Clerk, Stripe, Prisma, tRPC, Hono, GraphQL, Convex, Turso, Uploadthing, AI SDK, and the full AI-generated stack.",
6
6
  "type": "module",