guardvibe 1.9.0 → 1.9.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
@@ -63,8 +63,54 @@ function setupPlatform(name) {
63
63
  });
64
64
  }
65
65
  console.log(` [OK] Added MCP server to ${platform.description}`);
66
+ // For Claude Code: also set up hooks and CLAUDE.md guidance
67
+ if (name === "claude") {
68
+ setupClaudeHooksAndGuide();
69
+ }
66
70
  return true;
67
71
  }
72
+ function setupClaudeHooksAndGuide() {
73
+ // Add hook to .claude/settings.json for auto-scan after file edits
74
+ const claudeSettingsDir = join(process.cwd(), ".claude");
75
+ if (!existsSync(claudeSettingsDir)) {
76
+ mkdirSync(claudeSettingsDir, { recursive: true });
77
+ }
78
+ const claudeSettingsPath = join(claudeSettingsDir, "settings.json");
79
+ const existingSettings = readJsonFile(claudeSettingsPath) || {};
80
+ if (!existingSettings.hooks) {
81
+ existingSettings.hooks = {};
82
+ }
83
+ // Don't overwrite existing hooks
84
+ if (!existingSettings.hooks.PostToolUse) {
85
+ existingSettings.hooks.PostToolUse = [
86
+ {
87
+ matcher: "Edit|Write",
88
+ hooks: [
89
+ {
90
+ type: "command",
91
+ command: "echo 'Run guardvibe scan_file on edited files for security check'"
92
+ }
93
+ ]
94
+ }
95
+ ];
96
+ }
97
+ writeJsonFile(claudeSettingsPath, existingSettings);
98
+ console.log(` [OK] Claude Code hooks configured (.claude/settings.json)`);
99
+ // Add CLAUDE.md guidance if not present
100
+ const claudeMdPath = join(process.cwd(), "CLAUDE.md");
101
+ 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`;
102
+ if (existsSync(claudeMdPath)) {
103
+ const content = readFileSync(claudeMdPath, "utf-8");
104
+ if (!content.includes("GuardVibe")) {
105
+ writeFileSync(claudeMdPath, content + guardvibeBlock, "utf-8");
106
+ console.log(` [OK] GuardVibe guidance added to CLAUDE.md`);
107
+ }
108
+ }
109
+ else {
110
+ writeFileSync(claudeMdPath, `# Project Guidelines\n${guardvibeBlock}`, "utf-8");
111
+ console.log(` [OK] Created CLAUDE.md with GuardVibe guidance`);
112
+ }
113
+ }
68
114
  // ── Pre-commit hook ──────────────────────────────────────────────────
69
115
  const HOOK_SCRIPT = `#!/bin/sh
70
116
  # GuardVibe pre-commit security hook
@@ -111,4 +111,16 @@ export const databaseRules = [
111
111
  fixCode: '-- GOOD: view respects RLS\nCREATE VIEW user_orders\n WITH (security_invoker = true)\n AS SELECT * FROM orders;\n\n-- BAD: bypasses RLS\n-- CREATE VIEW user_orders AS SELECT * FROM orders;',
112
112
  compliance: ["SOC2:CC6.1"],
113
113
  },
114
+ {
115
+ id: "VG448",
116
+ name: "Supabase RPC Call May Bypass RLS",
117
+ severity: "high",
118
+ owasp: "A01:2025 Broken Access Control",
119
+ description: "supabase.rpc() calls execute PostgreSQL functions which may bypass Row Level Security if the function is defined as SECURITY DEFINER or if called with the service role key. Ensure the function uses SECURITY INVOKER or validate permissions manually.",
120
+ pattern: /supabase\.rpc\s*\(\s*["']\w+["']/g,
121
+ languages: ["javascript", "typescript"],
122
+ fix: "Ensure PostgreSQL functions called via rpc() use SECURITY INVOKER, or add explicit permission checks. Never call rpc() with the service role key from client-accessible code.",
123
+ fixCode: '-- PostgreSQL: use SECURITY INVOKER\nCREATE OR REPLACE FUNCTION get_user_data(user_id uuid)\nRETURNS TABLE (id uuid, name text)\nLANGUAGE sql\nSECURITY INVOKER -- respects RLS!\nAS $$\n SELECT id, name FROM users WHERE id = user_id;\n$$;\n\n// TypeScript: call with anon key (not service role)\nconst { data } = await supabase.rpc("get_user_data", { user_id: userId });',
124
+ compliance: ["SOC2:CC6.1"],
125
+ },
114
126
  ];
@@ -78,7 +78,7 @@ export const nextjsRules = [
78
78
  severity: "high",
79
79
  owasp: "A03:2025 Injection",
80
80
  description: "Dynamic route parameters (params, searchParams) are used directly in database queries or operations without validation.",
81
- pattern: /(?:params|searchParams)\s*[\.\[]\s*["']?\w+["']?\s*[\]\)]?\s*(?:;|\))\s*[\s\S]*?(?:query|execute|findUnique|findFirst|findMany|delete|update|create)\s*\(/g,
81
+ pattern: /(?:(?:await\s+)?(?:params|searchParams))\s*[\)\.\[]\s*["']?\w+["']?\s*[\]\)]?\s*(?:;|\))\s*[\s\S]*?(?:query|execute|findUnique|findFirst|findMany|delete|update|create)\s*\(/g,
82
82
  languages: ["javascript", "typescript"],
83
83
  fix: "Always validate and sanitize dynamic route parameters before using them in queries.",
84
84
  fixCode: '// Validate params before use\nimport { z } from "zod";\nconst idSchema = z.string().uuid();\n\nexport default async function Page({ params }: { params: { id: string } }) {\n const id = idSchema.parse((await params).id);\n const item = await db.item.findUnique({ where: { id } });\n}',
@@ -90,7 +90,7 @@ export const nextjsRules = [
90
90
  severity: "high",
91
91
  owasp: "A01:2025 Broken Access Control",
92
92
  description: "Sensitive data (tokens, secrets, internal IDs) appears to be passed from server to client component as props.",
93
- pattern: /(?:secret|token|password|apiKey|privateKey|internalId|ssn|creditCard)\s*=\s*\{[\s\S]*?\}/g,
93
+ pattern: /(?:(?:^|[^a-zA-Z])(?:secret|token|password|apiKey|api_key|privateKey|private_key|internalId|ssn|creditCard|credit_card))\s*=\s*\{[\s\S]*?\}/g,
94
94
  languages: ["javascript", "typescript"],
95
95
  fix: "Never pass sensitive data as props to client components. Keep secrets server-side.",
96
96
  fixCode: "// Keep sensitive data server-side\nexport default async function Page() {\n const secret = process.env.API_SECRET;\n const publicData = await fetchData(secret);\n return <ClientComponent data={publicData} />;\n}",
@@ -150,7 +150,7 @@ export const nextjsRules = [
150
150
  severity: "high",
151
151
  owasp: "A01:2025 Broken Access Control",
152
152
  description: "Server Action returns a full database query result without field selection. Sensitive fields (passwordHash, internalNotes) get serialized to the client.",
153
- pattern: /["']use server["'][\s\S]{0,800}?(?:findUnique|findFirst|findMany)\s*\([^)]*\)\s*;?\s*\n\s*return/g,
153
+ pattern: /["']use server["'][\s\S]{0,800}?(?:return\s+\w+\.)?(?:findUnique|findFirst|findMany)\s*\((?:(?!select\s*:)[\s\S])*?\)/g,
154
154
  languages: ["javascript", "typescript"],
155
155
  fix: "Always use select to return only needed fields from Server Actions.",
156
156
  fixCode: '"use server";\nexport async function getUser(id: string) {\n return prisma.user.findUnique({\n where: { id },\n select: { id: true, name: true, email: true },\n });\n}',
@@ -37,6 +37,40 @@ function isInComment(lines, lineNumber) {
37
37
  trimmed.startsWith("<!--") ||
38
38
  trimmed.startsWith("/*"));
39
39
  }
40
+ /**
41
+ * Check if a match is inside a multi-line string literal (template literal,
42
+ * fixCode/description property, or string concatenation).
43
+ * This prevents rule definition files, docs, and test fixtures from triggering
44
+ * false positives when they contain code examples as string values.
45
+ */
46
+ function isInsideStringLiteral(lines, lineNumber, code, matchIndex) {
47
+ const line = lines[lineNumber - 1];
48
+ if (!line)
49
+ return false;
50
+ const trimmed = line.trimStart();
51
+ // Line is part of a template literal or multi-line string
52
+ // Check if we're between backticks by counting unescaped backticks before this point
53
+ const before = code.substring(0, matchIndex);
54
+ const backtickCount = (before.match(/(?<!\\)`/g) || []).length;
55
+ if (backtickCount % 2 === 1)
56
+ return true; // inside template literal
57
+ // Line is a string property value (fixCode, description, fix, exploit, audit, fixCode)
58
+ // Look backwards to find if this line is inside a property assignment string
59
+ for (let i = lineNumber - 1; i >= Math.max(0, lineNumber - 20); i--) {
60
+ const prev = lines[i]?.trimStart() || "";
61
+ // Property assignment with string value that likely spans lines
62
+ if (/^(?:fixCode|fix|description|exploit|audit|fixCode)\s*[:=]/.test(prev))
63
+ return true;
64
+ if (/^(?:fixCode|fix|description|exploit|audit)\s*:\s*$/.test(prev))
65
+ return true;
66
+ // Hit a rule boundary (id: "VG...) — stop looking
67
+ if (/^\s*id\s*:\s*["']VG/.test(prev))
68
+ break;
69
+ if (/^\s*\{/.test(prev) && i < lineNumber - 2)
70
+ break;
71
+ }
72
+ return false;
73
+ }
40
74
  /**
41
75
  * Check if a match on a given line is inside a string value used as a
42
76
  * human-readable message (UI label, error text) rather than an actual secret.
@@ -101,8 +135,9 @@ export function analyzeCode(code, language, framework, filePath, configDir, rule
101
135
  if (rule.id.startsWith("VG54") && filePath && /(?:migrations?|seeds?|fixtures)\//i.test(filePath))
102
136
  continue;
103
137
  // Skip server-only import rule (VG964) for files that are inherently server-only:
104
- // Route Handlers (app/api/), middleware, instrumentation, next.config
105
- if (rule.id === "VG964" && filePath && /(?:\/api\/|middleware\.|instrumentation\.|next\.config\.)/.test(filePath))
138
+ // Route Handlers (app/api/), middleware, instrumentation, next.config,
139
+ // lib/, utils/, tools/, server/, scripts/, CLI files, config files
140
+ if (rule.id === "VG964" && filePath && /(?:\/api\/|middleware\.|instrumentation\.|next\.config\.|\/lib\/|\/utils\/|\/tools\/|\/server\/|\/scripts\/|\/src\/(?!app\/|pages\/|components\/)|\bcli\b|\.config\.)/.test(filePath))
106
141
  continue;
107
142
  // Skip React Native/mobile-only rules (VG70x) in web projects:
108
143
  // only apply when framework is react-native/expo or path suggests mobile
@@ -145,6 +180,12 @@ export function analyzeCode(code, language, framework, filePath, configDir, rule
145
180
  if (isInComment(lines, lineNumber))
146
181
  continue;
147
182
  }
183
+ // Skip matches inside string literals (fixCode, description, template strings)
184
+ // This prevents rule definition files and docs from triggering false positives
185
+ if (!rule.id.startsWith("VG9")) {
186
+ if (isInsideStringLiteral(lines, lineNumber, code, match.index))
187
+ continue;
188
+ }
148
189
  // Skip hardcoded-credential rules when the value is a human-readable sentence
149
190
  if (rule.id === "VG001" || rule.id === "VG062") {
150
191
  if (isHumanReadableString(lines, lineNumber))
@@ -218,6 +259,23 @@ function isDuplicatePair(a, b) {
218
259
  return true;
219
260
  if (a.rule.name.includes("Unsafe innerHTML") && b.rule.name.includes("XSS via innerHTML"))
220
261
  return true;
262
+ // Both are auth/unprotected route rules — VG420+VG952+VG002 duplicate case
263
+ const authPatterns = ["Unprotected Route", "Without Authentication", "Missing authentication"];
264
+ const aIsAuth = authPatterns.some(p => a.rule.name.includes(p));
265
+ const bIsAuth = authPatterns.some(p => b.rule.name.includes(p));
266
+ if (aIsAuth && bIsAuth)
267
+ return true;
268
+ // Both are CORS wildcard rules — VG040+VG403+VG973 duplicate case
269
+ const aIsCors = a.rule.name.includes("CORS") && a.rule.name.includes("ildcard");
270
+ const bIsCors = b.rule.name.includes("CORS") && b.rule.name.includes("ildcard");
271
+ if (aIsCors && bIsCors)
272
+ return true;
273
+ // Both are admin role check rules — VG426+VG957 duplicate case
274
+ const adminPatterns = ["Admin", "Role Check", "Role Verification"];
275
+ const aIsAdmin = adminPatterns.some(p => a.rule.name.includes(p));
276
+ const bIsAdmin = adminPatterns.some(p => b.rule.name.includes(p));
277
+ if (aIsAdmin && bIsAdmin)
278
+ return true;
221
279
  return false;
222
280
  }
223
281
  /** Check if rule A is more specific than rule B (framework rules > core rules). */
@@ -1,5 +1,5 @@
1
1
  import { readFileSync } from "fs";
2
- import { join, resolve } from "path";
2
+ import { join, resolve, dirname } from "path";
3
3
  const DEFAULT_CONFIG = {
4
4
  rules: { disable: [], severity: {} },
5
5
  scan: { exclude: [], maxFileSize: 500 * 1024 },
@@ -14,13 +14,38 @@ function cloneDefaultConfig() {
14
14
  plugins: [...DEFAULT_CONFIG.plugins],
15
15
  };
16
16
  }
17
+ /**
18
+ * Find .guardviberc by walking up from dir to filesystem root.
19
+ * Returns the path if found, null otherwise.
20
+ */
21
+ function findConfigFile(startDir) {
22
+ let current = startDir;
23
+ const root = resolve("/");
24
+ while (true) {
25
+ const candidate = join(current, ".guardviberc");
26
+ try {
27
+ readFileSync(candidate, "utf-8"); // will throw if not found
28
+ return candidate;
29
+ }
30
+ catch { }
31
+ const parent = dirname(current);
32
+ if (parent === current || current === root)
33
+ break;
34
+ current = parent;
35
+ }
36
+ return null;
37
+ }
17
38
  export function loadConfig(dir) {
18
39
  const configDir = resolve(dir || process.cwd());
19
40
  const cached = configCache.get(configDir);
20
41
  if (cached)
21
42
  return cached;
22
- const configPath = join(configDir, ".guardviberc");
43
+ const configPath = findConfigFile(configDir);
23
44
  let resolvedConfig = cloneDefaultConfig();
45
+ if (!configPath) {
46
+ configCache.set(configDir, resolvedConfig);
47
+ return resolvedConfig;
48
+ }
24
49
  try {
25
50
  const content = readFileSync(configPath, "utf-8");
26
51
  const parsed = JSON.parse(content);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "guardvibe",
3
- "version": "1.9.0",
3
+ "version": "1.9.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": {