guardvibe 3.0.17 → 3.0.19

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/hook.js CHANGED
@@ -11,7 +11,7 @@ const HOOK_SCRIPT = `#!/bin/sh
11
11
  echo "🔒 GuardVibe: scanning staged files..."
12
12
 
13
13
  # Run guardvibe scan on staged files
14
- RESULT=$(npx -y guardvibe-scan 2>&1)
14
+ RESULT=$(npx -y guardvibe scan --staged 2>&1)
15
15
  EXIT_CODE=$?
16
16
 
17
17
  if [ $EXIT_CODE -ne 0 ]; then
@@ -152,11 +152,22 @@ function runChecks(files, root) {
152
152
  const apiRoutes = files.routeHandlers
153
153
  .map(r => r.path.replace(resolve(root), "").replace(/\\/g, "/"))
154
154
  .filter(p => p.includes("/api/"));
155
+ // Check which routes are NOT covered by middleware matcher
156
+ // But exclude routes that have in-handler auth (requireAdmin, requireAuth, etc.)
157
+ const authGuardPattern = /requireAdmin|requireAuth|checkAuth|withAuth|getServerSession|auth\(\)|clerkClient|currentUser/;
155
158
  const unprotectedApiRoutes = apiRoutes.filter(route => {
156
- return !matcherPaths.some(pattern => {
159
+ // Check if middleware matcher covers this route
160
+ const coveredByMatcher = matcherPaths.some(pattern => {
157
161
  const normalized = pattern.replace(/:path\*/, "").replace(/\(.*?\)/, "");
158
162
  return route.startsWith(normalized) || route.includes(normalized);
159
163
  });
164
+ if (coveredByMatcher)
165
+ return false;
166
+ // Check if the route handler has in-handler auth guard
167
+ const handler = files.routeHandlers.find(r => r.path.replace(resolve(root), "").replace(/\\/g, "/") === route);
168
+ if (handler && authGuardPattern.test(handler.content))
169
+ return false;
170
+ return true;
160
171
  });
161
172
  if (unprotectedApiRoutes.length > 0) {
162
173
  issues.push({
@@ -193,11 +204,23 @@ function runChecks(files, root) {
193
204
  });
194
205
  }
195
206
  // Check for secrets in .env files that are also in NEXT_PUBLIC_
207
+ // Known safe public keys that are DESIGNED to be in client bundles
208
+ const knownPublicKeys = new Set([
209
+ "NEXT_PUBLIC_SUPABASE_ANON_KEY",
210
+ "NEXT_PUBLIC_SUPABASE_URL",
211
+ "NEXT_PUBLIC_TURNSTILE_SITE_KEY",
212
+ "NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY",
213
+ "NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY",
214
+ "NEXT_PUBLIC_RECAPTCHA_SITE_KEY",
215
+ "NEXT_PUBLIC_GA_MEASUREMENT_ID",
216
+ "NEXT_PUBLIC_SITE_URL",
217
+ "NEXT_PUBLIC_APP_URL",
218
+ ]);
196
219
  for (const envFile of files.envFiles) {
197
220
  const lines = envFile.content.split("\n");
198
221
  for (const line of lines) {
199
222
  const match = /^(NEXT_PUBLIC_\w*(?:SECRET|KEY|PASSWORD|TOKEN|PRIVATE|CREDENTIAL)\w*)\s*=/.exec(line);
200
- if (match && !/PUBLISHABLE/i.test(match[1])) {
223
+ if (match && !/PUBLISHABLE/i.test(match[1]) && !knownPublicKeys.has(match[1])) {
201
224
  issues.push({
202
225
  id: "AC021", severity: "critical", category: "secrets",
203
226
  title: `NEXT_PUBLIC_ exposes secret: ${match[1]}`,
@@ -5,18 +5,23 @@ import { loadIgnoreFile, isIgnored } from "../utils/ignore.js";
5
5
  import { securityBanner } from "../utils/banner.js";
6
6
  function parseSuppressionsFromCode(lines) {
7
7
  const suppressions = [];
8
- const pattern = /(?:\/\/|#|<!--)\s*guardvibe-ignore(?:-next-line)?\s*(VG\d+)?\s*(?:-->)?/i;
8
+ const pattern = /(?:\/\/|#|<!--)\s*guardvibe-ignore(?:-next-line)?\s*(VG\d+)?(?:\s.*)?(?:-->)?/i;
9
9
  for (let i = 0; i < lines.length; i++) {
10
10
  const match = pattern.exec(lines[i]);
11
11
  if (!match)
12
12
  continue;
13
13
  const ruleId = match[1] || null;
14
14
  const isNextLine = lines[i].includes("guardvibe-ignore-next-line");
15
+ const isCommentOnlyLine = /^\s*(?:\/\/|#|<!--)/.test(lines[i]);
15
16
  if (isNextLine) {
16
- suppressions.push({ line: i + 2, ruleId }); // next line (1-indexed)
17
+ suppressions.push({ line: i + 2, ruleId });
18
+ }
19
+ else if (isCommentOnlyLine) {
20
+ suppressions.push({ line: i + 1, ruleId });
21
+ suppressions.push({ line: i + 2, ruleId });
17
22
  }
18
23
  else {
19
- suppressions.push({ line: i + 1, ruleId }); // same line (1-indexed)
24
+ suppressions.push({ line: i + 1, ruleId });
20
25
  }
21
26
  }
22
27
  return suppressions;
@@ -280,6 +280,18 @@ const SINK_PATTERNS = [
280
280
  { pattern: /writeFileSync?\s*\(/g, type: "path-traversal" },
281
281
  { pattern: /readFileSync?\s*\(/g, type: "path-traversal" },
282
282
  ];
283
+ // Patterns that break the taint chain (validation/sanitization)
284
+ const SANITIZER_PATTERNS = [
285
+ /validate\w*\s*\(/i,
286
+ /sanitize\w*\s*\(/i,
287
+ /safeParse\s*\(/i,
288
+ /parseBody\s*\(/i,
289
+ /DOMPurify/i,
290
+ /encodeURIComponent\s*\(/i,
291
+ /\.hostname\s*!==?\s*/i,
292
+ /\.origin\s*!==?\s*/i,
293
+ /allowlist|whitelist|allowedHosts/i,
294
+ ];
283
295
  function checkParamFlowsToSink(paramName, body, startLine) {
284
296
  const lines = body.split("\n");
285
297
  const taintedNames = new Set([paramName]);
@@ -289,11 +301,20 @@ function checkParamFlowsToSink(paramName, body, startLine) {
289
301
  if (m) {
290
302
  for (const t of taintedNames) {
291
303
  if (m[2].includes(t)) {
292
- taintedNames.add(m[1]);
304
+ const isSanitized = SANITIZER_PATTERNS.some(p => p.test(m[2]));
305
+ if (!isSanitized) {
306
+ taintedNames.add(m[1]);
307
+ }
293
308
  break;
294
309
  }
295
310
  }
296
311
  }
312
+ // Break taint if value passes through validation
313
+ for (const t of taintedNames) {
314
+ if (line.includes(t) && SANITIZER_PATTERNS.some(p => p.test(line))) {
315
+ taintedNames.delete(t);
316
+ }
317
+ }
297
318
  }
298
319
  for (let i = 0; i < lines.length; i++) {
299
320
  const line = lines[i];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "guardvibe",
3
- "version": "3.0.17",
3
+ "version": "3.0.19",
4
4
  "mcpName": "io.github.goklab/guardvibe",
5
5
  "description": "Security MCP for vibe coding. 335 rules, 36 tools, CLI + doctor. Host security, auth coverage mapping, LLM-powered deep scan (IDOR/business logic), taint analysis. 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",