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 +18 -8
- package/build/tools/check-code.js +133 -11
- package/package.json +1 -1
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
|
-
|
|
137
|
-
|
|
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
|
-
|
|
262
|
-
|
|
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 &&
|
|
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
|
|
322
|
-
if (
|
|
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
|
|
331
|
-
if (rule.id === "
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
const
|
|
335
|
-
|
|
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.
|
|
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",
|