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 +20 -16
- package/build/tools/check-code.js +94 -8
- package/package.json +1 -1
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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 &&
|
|
352
|
+
if (codeHasUrlValidation && rule.id === "VG120")
|
|
327
353
|
continue;
|
|
328
|
-
// Skip
|
|
329
|
-
|
|
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
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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.
|
|
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",
|