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 +46 -0
- package/build/data/rules/database.js +12 -0
- package/build/data/rules/nextjs.js +3 -3
- package/build/tools/check-code.js +60 -2
- package/build/utils/config.js +27 -2
- package/package.json +1 -1
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*\([
|
|
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
|
-
|
|
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). */
|
package/build/utils/config.js
CHANGED
|
@@ -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 =
|
|
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.
|
|
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": {
|