guardvibe 2.0.0 → 2.0.2

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.js CHANGED
@@ -83,7 +83,8 @@ function setupClaudeHooksAndGuide() {
83
83
  if (!existingSettings.hooks) {
84
84
  existingSettings.hooks = {};
85
85
  }
86
- // Don't overwrite existing hooks
86
+ // Real security hook: scan edited files automatically after every Edit/Write
87
+ // This makes GuardVibe an active guardian, not a passive tool
87
88
  if (!existingSettings.hooks.PostToolUse) {
88
89
  existingSettings.hooks.PostToolUse = [
89
90
  {
@@ -91,7 +92,7 @@ function setupClaudeHooksAndGuide() {
91
92
  hooks: [
92
93
  {
93
94
  type: "command",
94
- command: "echo 'Run guardvibe scan_file on edited files for security check'"
95
+ command: "npx -y guardvibe check $CLAUDE_FILE_PATH --format markdown 2>/dev/null || true"
95
96
  }
96
97
  ]
97
98
  }
@@ -101,7 +102,28 @@ function setupClaudeHooksAndGuide() {
101
102
  console.log(` [OK] Claude Code hooks configured (.claude/settings.json)`);
102
103
  // Add CLAUDE.md guidance if not present
103
104
  const claudeMdPath = join(process.cwd(), "CLAUDE.md");
104
- const guardvibeBlock = `\n## GuardVibe Security\n\n- After editing security-sensitive files (API routes, auth, middleware), use the \`scan_file\` MCP tool to check for vulnerabilities\n- Before committing, run \`scan_staged\` to catch issues in staged files\n- Use \`scan_changed_files\` with the base branch to review all changes\n- When you find issues, use \`explain_remediation\` for detailed fix guidance\n`;
105
+ const guardvibeBlock = `
106
+ ## Security Rules (enforced by GuardVibe MCP)
107
+
108
+ ### Mandatory checks
109
+ - BEFORE writing any API route, Server Action, or database query: call \`scan_file\` on the file after editing
110
+ - BEFORE committing: call \`scan_staged\` to catch issues in staged files
111
+ - When GuardVibe reports HIGH or CRITICAL: fix immediately before moving on
112
+
113
+ ### Writing secure code
114
+ - Every API route handler MUST have auth check before DB access
115
+ - Every POST endpoint MUST have input validation (zod/joi schema)
116
+ - Never pass user input to \`innerHTML\`, \`eval\`, \`exec\`, or raw SQL
117
+ - Never pass user-controlled URLs to \`fetch()\` without allowlist validation
118
+ - Always use \`select:\` in Prisma queries from Server Actions (never return full objects)
119
+ - Always validate redirect URLs against trusted domain allowlist
120
+ - Set security headers in next.config.ts (CSP, HSTS, X-Frame-Options)
121
+
122
+ ### When in doubt
123
+ - Use \`explain_remediation\` with the rule ID for detailed fix guidance
124
+ - Use \`scan_changed_files\` with base branch before creating PR
125
+ - Use \`check_code\` to verify a code snippet is secure before applying
126
+ `;
105
127
  if (existsSync(claudeMdPath)) {
106
128
  const content = readFileSync(claudeMdPath, "utf-8");
107
129
  if (!content.includes("GuardVibe")) {
@@ -392,7 +392,7 @@ export const coreRules = [
392
392
  severity: "high",
393
393
  owasp: "A10:2025 Server-Side Request Forgery",
394
394
  description: "User-controlled input is passed directly to fetch(), axios, or http.request() as the URL. Attackers can make the server request internal services (169.254.169.254 for cloud metadata, localhost admin panels, internal APIs) leading to data exfiltration or remote code execution.",
395
- pattern: /(?:fetch|axios\.(?:get|post|put|delete|patch|request)|got(?:\.(?:get|post|put|delete|patch))?|request|http\.(?:get|request)|https\.(?:get|request)|urllib\.request\.urlopen)\s*\(\s*(?:url|uri|href|endpoint|target|destination|webhook[Uu]rl|callback[Uu]rl|redirect[Uu]rl|image[Uu]rl|proxy[Uu]rl|api[Uu]rl|base[Uu]rl|link|src|source|remoteUrl|externalUrl|userUrl|inputUrl|requestUrl)\b/gi,
395
+ pattern: /(?:fetch|axios\.(?:get|post|put|delete|patch|request)|got(?:\.(?:get|post|put|delete|patch))?|http\.(?:get|request)|https\.(?:get|request)|urllib\.request\.urlopen)\s*\(\s*(?!["'`]https?:\/\/|["'`]\/|`\$\{process\.env)(?:[a-zA-Z_$]\w*)\s*[,)]/gi,
396
396
  languages: ["javascript", "typescript", "python"],
397
397
  fix: "Validate URLs against an allowlist of trusted domains. Block private/internal IP ranges (10.x, 172.16-31.x, 192.168.x, 127.x, 169.254.x, ::1). Use a URL parser to check the hostname before making the request.",
398
398
  fixCode: '// Validate URL before fetching\nconst ALLOWED_HOSTS = ["api.example.com", "cdn.example.com"];\nconst parsed = new URL(userUrl);\nif (!ALLOWED_HOSTS.includes(parsed.hostname)) {\n throw new Error("Blocked: untrusted host");\n}\n// Also block private IPs\nconst ip = await dns.resolve(parsed.hostname);\nif (isPrivateIP(ip)) throw new Error("Blocked: internal host");\nawait fetch(parsed.href);',
@@ -126,6 +126,53 @@ function isRuleDefinitionFile(code, filePath) {
126
126
  }
127
127
  return false;
128
128
  }
129
+ /**
130
+ * Detect if code contains an auth guard pattern — regardless of function name.
131
+ * Matches patterns like:
132
+ * const { userId } = await someFunction(); if (!userId) return/throw;
133
+ * const { error } = await someFunction(); if (error) return error;
134
+ * const session = await someFunction(); if (!session) throw/return;
135
+ * await someFunction(); // + early return pattern
136
+ *
137
+ * This is naming-agnostic: works for requireAdmin, verifyAuth, checkPermission,
138
+ * ensureLoggedIn, or any custom auth wrapper.
139
+ */
140
+ function hasAuthGuardPattern(code) {
141
+ // Pattern 1: destructured result checked with early return/throw
142
+ // e.g., const { userId } = await xxx(); if (!userId) return;
143
+ // e.g., const { error } = await xxx(); if (error) return error;
144
+ if (/(?:const|let)\s+\{[^}]*\}\s*=\s*await\s+\w+\s*\([^)]*\)\s*;?\s*\n\s*if\s*\(\s*!?\w+/.test(code)) {
145
+ if (/if\s*\([^)]*\)\s*(?:return|throw)\b/.test(code))
146
+ return true;
147
+ }
148
+ // Pattern 2: result assigned then checked
149
+ // e.g., const session = await xxx(); if (!session) return;
150
+ if (/(?:const|let)\s+\w+\s*=\s*await\s+\w+\s*\([^)]*\)\s*;?\s*\n\s*if\s*\(\s*!\w+/.test(code)) {
151
+ return true;
152
+ }
153
+ // Pattern 3: function called with await that contains auth-like keywords in name
154
+ // Broad catch: any function name containing auth/session/permission/guard/verify/protect
155
+ if (/await\s+(?:\w+\.)*\w*(?:auth|Auth|session|Session|permission|Permission|guard|Guard|verify|Verify|protect|Protect|check|Check|ensure|Ensure|require|Require|assert|Assert|authorize|Authorize)\w*\s*\(/i.test(code)) {
156
+ return true;
157
+ }
158
+ return false;
159
+ }
160
+ /**
161
+ * Detect if code has a role/permission check — regardless of function name.
162
+ * Matches: role === "admin", permission check, role-based condition.
163
+ */
164
+ function hasRoleCheckPattern(code) {
165
+ // Direct role/permission comparison
166
+ if (/(?:role|permission|isAdmin|access|level)\s*(?:===|!==|==|!=)\s*["']/i.test(code))
167
+ return true;
168
+ // Function call with role/permission-like args
169
+ if (/(?:check|require|verify|ensure|assert|has|can)\w*\s*\(\s*["'](?:admin|manager|editor|owner|moderator|superadmin)/i.test(code))
170
+ return true;
171
+ // Destructured role check: const { role } = ...; if (role !== "admin")
172
+ if (/\brole\b[\s\S]{0,100}?(?:!==|===)\s*["']/i.test(code))
173
+ return true;
174
+ return false;
175
+ }
129
176
  export function analyzeCode(code, language, framework, filePath, configDir, rules) {
130
177
  // Skip files that are security rule definitions (they intentionally contain
131
178
  // vulnerable code patterns as regex matchers and fixCode examples)
@@ -136,6 +183,15 @@ export function analyzeCode(code, language, framework, filePath, configDir, rule
136
183
  const findings = [];
137
184
  const lines = code.split("\n");
138
185
  const suppressions = parseSuppressionsFromCode(lines);
186
+ // Pre-analyze: detect auth guards and role checks pattern-agnostically
187
+ let codeHasAuthGuard = hasAuthGuardPattern(code);
188
+ const codeHasRoleCheck = hasRoleCheckPattern(code);
189
+ // Config: check custom auth function names from .guardviberc
190
+ if (!codeHasAuthGuard && config.authFunctions && config.authFunctions.length > 0) {
191
+ const customPattern = new RegExp(`(?:${config.authFunctions.join("|")})\\s*\\(`, "i");
192
+ if (customPattern.test(code))
193
+ codeHasAuthGuard = true;
194
+ }
139
195
  const effectiveRules = rules ?? owaspRules;
140
196
  for (const rule of effectiveRules) {
141
197
  if (!rule.languages.includes(language))
@@ -152,25 +208,30 @@ export function analyzeCode(code, language, framework, filePath, configDir, rule
152
208
  continue;
153
209
  if (rule.id.startsWith("VG21") && !filePath && language !== "yaml")
154
210
  continue;
155
- // Context-aware: skip auth rules for webhook routes that have signature verification
211
+ // ── Context-aware rule skipping (pattern-agnostic) ──────────────
212
+ const authRuleIds = new Set(["VG420", "VG952", "VG002", "VG402"]);
213
+ const adminRoleRuleIds = new Set(["VG426", "VG957"]);
214
+ const rateLimitRuleIds = new Set(["VG956", "VG030"]);
156
215
  const isWebhookRoute = filePath && /webhook/i.test(filePath);
216
+ const isCronRoute = filePath && /(?:cron|scheduled|jobs?)\//i.test(filePath);
217
+ const isAdminRoute = filePath && /\/admin\//i.test(filePath);
218
+ // Skip auth rules when code has any auth guard pattern (naming-agnostic)
219
+ if (codeHasAuthGuard && authRuleIds.has(rule.id))
220
+ continue;
221
+ // Skip admin role rules when code has any role/permission check
222
+ if (codeHasRoleCheck && adminRoleRuleIds.has(rule.id))
223
+ continue;
224
+ // Skip auth rules for webhook routes with signature verification
157
225
  const hasSignatureVerification = isWebhookRoute && /(?:verify|signature|hmac|constructEvent|svix|webhookSecret|createHmac|X-Signature|stripe-signature)/i.test(code);
158
- const authRuleIds = new Set(["VG420", "VG952", "VG002"]);
159
226
  if (hasSignatureVerification && authRuleIds.has(rule.id))
160
227
  continue;
161
- // Context-aware: skip rate limiting rules for cron/scheduled routes
162
- const isCronRoute = filePath && /(?:cron|scheduled|jobs?)\//i.test(filePath);
163
- const rateLimitRuleIds = new Set(["VG956", "VG030"]);
228
+ // Skip rate limiting for cron and webhook routes
164
229
  if (isCronRoute && rateLimitRuleIds.has(rule.id))
165
230
  continue;
166
- // Context-aware: skip rate limiting rules for webhook routes
167
- // Webhooks are called by external services, not users — rate limiting is irrelevant
168
231
  if (isWebhookRoute && rateLimitRuleIds.has(rule.id))
169
232
  continue;
170
- // Context-aware: skip rate limiting rules for admin routes that have admin auth
171
- const isAdminRoute = filePath && /\/admin\//i.test(filePath);
172
- const hasAdminAuth = isAdminRoute && /(?:requireAdmin|adminOnly|orgRole|org:admin|isAdmin|checkRole|requireRole|verifyAuth|checkPermission|assertAuth|ensureAuth|authorize)/i.test(code);
173
- if (hasAdminAuth && rateLimitRuleIds.has(rule.id))
233
+ // Skip rate limiting for admin routes with auth guard
234
+ if (isAdminRoute && codeHasAuthGuard && rateLimitRuleIds.has(rule.id))
174
235
  continue;
175
236
  // Skip npm package rules (VG863/VG864/VG865): only apply to package.json files
176
237
  if ((rule.id === "VG863" || rule.id === "VG864" || rule.id === "VG865") && filePath && !filePath.endsWith("package.json"))
@@ -199,7 +260,7 @@ export function analyzeCode(code, language, framework, filePath, configDir, rule
199
260
  : rule;
200
261
  // Context-aware severity: downgrade rate limiting/pagination issues in admin routes
201
262
  // Admin routes behind requireAdmin have lower brute-force risk
202
- if (isAdminRoute && hasAdminAuth) {
263
+ if (isAdminRoute && codeHasAuthGuard) {
203
264
  const downgradeInAdmin = new Set(["VG955"]); // pagination in admin is less critical
204
265
  if (downgradeInAdmin.has(rule.id) && effectiveRule.severity === "medium") {
205
266
  effectiveRule = { ...effectiveRule, severity: "low" };
@@ -22,6 +22,10 @@ export interface GuardVibeConfig {
22
22
  };
23
23
  plugins: string[];
24
24
  compliance?: CompliancePolicy;
25
+ /** Custom auth function names that GuardVibe should recognize as auth guards.
26
+ * e.g. ["requireAdmin", "verifyUser", "ensureLoggedIn"]
27
+ * These are added ON TOP of the built-in pattern-agnostic detection. */
28
+ authFunctions?: string[];
25
29
  }
26
30
  export declare function loadConfig(dir?: string): GuardVibeConfig;
27
31
  export declare function resetConfigCache(): void;
@@ -67,6 +67,7 @@ export function loadConfig(dir) {
67
67
  exceptions: Array.isArray(parsed.compliance.exceptions) ? parsed.compliance.exceptions : [],
68
68
  requiredControls: Array.isArray(parsed.compliance.requiredControls) ? parsed.compliance.requiredControls : undefined,
69
69
  } : undefined,
70
+ authFunctions: Array.isArray(parsed.authFunctions) ? parsed.authFunctions : undefined,
70
71
  };
71
72
  }
72
73
  catch { }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "guardvibe",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
4
4
  "description": "Security MCP for vibe coding. 277 rules, 24 tools for Next.js, Supabase, Clerk, Stripe, Prisma, tRPC, Hono, GraphQL, Convex, Turso, Uploadthing, AI SDK, and the full AI-generated stack.",
5
5
  "type": "module",
6
6
  "bin": {