guardvibe 2.9.6 → 2.9.8

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/cli/scan.js CHANGED
@@ -34,7 +34,7 @@ export async function runScan() {
34
34
  else {
35
35
  console.log(result);
36
36
  }
37
- if (format !== "sarif") {
37
+ if (format !== "sarif" && flags["fail-on"]) {
38
38
  const failOn = getStringFlag(flags, "fail-on") ?? "critical";
39
39
  if (shouldFail(result, failOn))
40
40
  process.exit(1);
@@ -67,7 +67,7 @@ export async function runDirectoryScan(targetPath, flags) {
67
67
  : join(scanPath, ".guardvibe-baseline.json");
68
68
  safeWriteOutput(baselineFile, result);
69
69
  }
70
- if (format !== "sarif") {
70
+ if (format !== "sarif" && flags["fail-on"]) {
71
71
  const failOn = getStringFlag(flags, "fail-on") ?? "critical";
72
72
  if (shouldFail(result, failOn))
73
73
  process.exit(1);
@@ -150,17 +150,19 @@ export async function runDiffScan(base, flags) {
150
150
  else {
151
151
  console.log(result);
152
152
  }
153
- const failOn = getStringFlag(flags, "fail-on") ?? "critical";
154
- if (failOn !== "none") {
155
- const failLevels = {
156
- low: ["critical", "high", "medium", "low"],
157
- medium: ["critical", "high", "medium"],
158
- high: ["critical", "high"],
159
- critical: ["critical"],
160
- };
161
- const levels = failLevels[failOn] || failLevels.critical;
162
- if (allFindings.some(f => levels.includes(f.severity)))
163
- process.exit(1);
153
+ if (flags["fail-on"]) {
154
+ const failOn = getStringFlag(flags, "fail-on") ?? "critical";
155
+ if (failOn !== "none") {
156
+ const failLevels = {
157
+ low: ["critical", "high", "medium", "low"],
158
+ medium: ["critical", "high", "medium"],
159
+ high: ["critical", "high"],
160
+ critical: ["critical"],
161
+ };
162
+ const levels = failLevels[failOn] || failLevels.critical;
163
+ if (allFindings.some(f => levels.includes(f.severity)))
164
+ process.exit(1);
165
+ }
164
166
  }
165
167
  }
166
168
  export async function runFileCheck(filePath, flags) {
@@ -196,9 +198,11 @@ export async function runFileCheck(filePath, flags) {
196
198
  else {
197
199
  console.log(result);
198
200
  }
199
- const failOn = getStringFlag(flags, "fail-on") ?? "critical";
200
- if (shouldFail(result, failOn))
201
- process.exit(1);
201
+ if (flags["fail-on"]) {
202
+ const failOn = getStringFlag(flags, "fail-on") ?? "critical";
203
+ if (shouldFail(result, failOn))
204
+ process.exit(1);
205
+ }
202
206
  }
203
207
  export async function handleScanCommand(args) {
204
208
  const { flags, positional } = parseArgs(args);
@@ -209,6 +209,9 @@ const LEGITIMATE_PREFIXED_PACKAGES = new Set([
209
209
  "original-url", "original-fs",
210
210
  "secure-json-parse",
211
211
  "native-run",
212
+ "fast-sha256", "fast-text-encoding",
213
+ "svix",
214
+ "cheerio",
212
215
  ]);
213
216
  function isLegitimatePackage(name) {
214
217
  return LEGITIMATE_PREFIXED_PACKAGES.has(name);
@@ -265,6 +268,10 @@ export function analyzeCode(code, language, framework, filePath, configDir, rule
265
268
  const codeHasRedirectValidation = /(?:sanitize|validate|verify|check|safe|allowed)(?:Redirect|RedirectUrl|CallbackUrl)\s*\(/i.test(code) ||
266
269
  /import\s+.*(?:sanitizeRedirect|validateRedirect|safeRedirect)/i.test(code);
267
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);
268
275
  const isPeerDeps = /["']peerDependencies["']/i.test(code);
269
276
  // Config: check custom auth function names from .guardviberc
270
277
  if (!codeHasAuthGuard && config.authFunctions && config.authFunctions.length > 0) {
@@ -298,6 +305,10 @@ export function analyzeCode(code, language, framework, filePath, configDir, rule
298
305
  // Skip auth rules when code has any auth guard pattern (naming-agnostic)
299
306
  if (codeHasAuthGuard && authRuleIds.has(rule.id))
300
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;
301
312
  // Skip admin role rules when code has any role/permission check
302
313
  if (codeHasRoleCheck && adminRoleRuleIds.has(rule.id))
303
314
  continue;
@@ -319,30 +330,82 @@ export function analyzeCode(code, language, framework, filePath, configDir, rule
319
330
  // Skip destructive DDL rules (VG540-VG542) and view rules (VG439) in migration directories
320
331
  if ((rule.id.startsWith("VG54") || rule.id === "VG439") && isMigrationFile)
321
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;
322
340
  // Skip innerHTML/XSS rules when DOMPurify or sanitization is present
323
- if (codeHasSanitization && ["VG408", "VG012", "VG042"].includes(rule.id))
341
+ if (codeHasSanitization && ["VG408", "VG012", "VG042", "VG852"].includes(rule.id))
324
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
+ }
325
351
  // Skip SSRF rules when URL validation/allowlist pattern is present
326
- if (codeHasUrlValidation && ["VG120"].includes(rule.id))
352
+ if (codeHasUrlValidation && rule.id === "VG120")
327
353
  continue;
328
- // Skip filename rules when UUID-based filename generation is present
329
- if (codeHasUuidFilename && rule.id === "VG993")
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")
367
+ continue;
368
+ // Skip timing-unsafe comparison rule when timingSafeEqual is already used in the file
369
+ if (codeHasTimingSafeEqual && rule.id === "VG106")
330
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
+ }
331
376
  // Skip cron secret rules when custom verification function is present
332
377
  if (codeHasCronVerification && ["VG968", "VG503"].includes(rule.id))
333
378
  continue;
334
379
  // Skip open redirect rules when redirect URL validation is present
335
380
  if (codeHasRedirectValidation && ["VG425", "VG409", "VG660"].includes(rule.id))
336
381
  continue;
337
- // Skip VG678 (Missing X-Content-Type-Options) in client components
338
- // client-side code calling getPublicUrl() can't set response headers.
339
- // Also skip when code only uses Supabase getPublicUrl/getSignedUrl to generate
340
- // URL strings (not actually serving file content via response stream).
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))
388
+ continue;
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
341
399
  if (rule.id === "VG678") {
342
400
  const isClientComponent = /^['"]use client['"]/.test(code.trimStart()) ||
343
401
  (filePath && /Client\.\w+$/.test(filePath));
344
402
  if (isClientComponent)
345
403
  continue;
404
+ if (isReactNative)
405
+ continue;
406
+ const isEmailFile = /(?:resend|nodemailer|sendEmail|sendMail|email\.send)/i.test(code);
407
+ if (isEmailFile)
408
+ continue;
346
409
  const hasOnlyUrlGeneration = /(?:getPublicUrl|getSignedUrl)\s*\(/i.test(code) &&
347
410
  !/(?:createReadStream|sendFile|res\.download|\.pipe\s*\()/i.test(code);
348
411
  if (hasOnlyUrlGeneration)
@@ -371,10 +434,27 @@ export function analyzeCode(code, language, framework, filePath, configDir, rule
371
434
  // Skip CVE version rules in peerDependencies (ranges, not actual versions)
372
435
  if (isPeerDeps && rule.id === "VG903")
373
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
+ }
374
443
  // Skip VG020 (wildcard dependency version) in lock files — engine constraints
375
444
  // like "node": ">=6" are not dependency versions
376
445
  if (rule.id === "VG020" && filePath && /(?:package-lock\.json|yarn\.lock|pnpm-lock\.yaml|npm-shrinkwrap\.json)$/.test(filePath))
377
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;
378
458
  // VG872/VG873 legitimate package filtering is handled at match level below
379
459
  // Skip server-only import rule (VG964) for files that are inherently server-only:
380
460
  // Route Handlers (app/api/), middleware, instrumentation, next.config,
@@ -439,6 +519,12 @@ export function analyzeCode(code, language, framework, filePath, configDir, rule
439
519
  if (pkgMatch && isLegitimatePackage(pkgMatch[1]))
440
520
  continue;
441
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
+ }
442
528
  // Skip VG903 React version in peerDependencies sections
443
529
  if (rule.id === "VG903") {
444
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.6",
3
+ "version": "2.9.8",
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",