guardvibe 3.7.0 → 3.8.0

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/CHANGELOG.md CHANGED
@@ -5,6 +5,16 @@ All notable changes to GuardVibe are documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [3.8.0] - 2026-06-07
9
+
10
+ ### Fixed — auth-coverage no longer crashes on (and now understands) Clerk/Next.js middleware (441 rules / 37 tools)
11
+ - **Crash fix:** the Next.js/Clerk catch-all `config.matcher` contains `]` inside character classes (e.g. `[^?]`), which truncated the old matcher parser and then made `matcherToRegex` throw "Unterminated character class" — so `auth_coverage` errored out on essentially every Clerk app. The matcher array is now parsed string-aware (brackets/commas/escapes inside a pattern are preserved) and matcher-to-regex never throws (it tries path-style and regex-style forms, skipping any it can't compile).
12
+ - **Precision:** when the middleware uses Clerk's `createRouteMatcher([...])`, those patterns are used as the precise protected-route set (a sensitive route outside the list is correctly still reported unprotected) instead of the broad `config.matcher` run-scope.
13
+ - **Fewer false negatives:** a recognizably non-auth middleware (next-intl / i18n / analytics) with a catch-all matcher no longer marks routes as protected. Default remains lenient for everything else, so custom auth middleware still counts.
14
+ - New exported `parseProtectedRouteMatchers`; verified on the corpus (5 real middleware files, 0 crashes). No rule or tool changes (441 / 37).
15
+
16
+ Gate green (build / lint / test / self-audit PASS / A / 0).
17
+
8
18
  ## [3.7.0] - 2026-06-07
9
19
 
10
20
  ### Added — 3 fresh CVE rules from daily intel (438 → 441 rules / 37 tools)
@@ -18,11 +18,14 @@ export interface FileEntry {
18
18
  * Enumerate all routes from a set of app directory files.
19
19
  */
20
20
  export declare function enumerateRoutes(files: FileEntry[]): RouteInfo[];
21
+ export declare function parseMiddlewareMatchers(content: string): string[];
21
22
  /**
22
- * Parse Next.js middleware config.matcher from middleware file content.
23
- * Returns array of matcher patterns.
23
+ * Clerk-style protect lists: `createRouteMatcher([...])`. When present these are the
24
+ * precise routes the middleware enforces auth on — more accurate than config.matcher
25
+ * (which only says where the middleware *runs*), so a sensitive route outside the
26
+ * protect list is correctly still reported as unprotected.
24
27
  */
25
- export declare function parseMiddlewareMatchers(content: string): string[];
28
+ export declare function parseProtectedRouteMatchers(content: string): string[];
26
29
  /**
27
30
  * Check if a route URL path matches any of the middleware matchers.
28
31
  * Empty matchers = middleware covers all routes.
@@ -82,35 +82,108 @@ export function enumerateRoutes(files) {
82
82
  * Parse Next.js middleware config.matcher from middleware file content.
83
83
  * Returns array of matcher patterns.
84
84
  */
85
+ function stripComments(content) {
86
+ return content
87
+ .replace(/\\n/g, "\n").replace(/\\t/g, "\t")
88
+ .replace(/\/\*[\s\S]*?\*\//g, "")
89
+ .replace(/\/\/.*$/gm, "");
90
+ }
91
+ /** Inner text of the array starting at `[` at `openIdx`, scanning string-aware so
92
+ * a `]` inside a string literal (e.g. the catch-all `[^?]`) doesn't end it early. */
93
+ function bracketInner(s, openIdx) {
94
+ let depth = 0;
95
+ for (let i = openIdx; i < s.length; i++) {
96
+ const ch = s[i];
97
+ if (ch === '"' || ch === "'" || ch === "`") {
98
+ const q = ch;
99
+ i++;
100
+ while (i < s.length && s[i] !== q) {
101
+ if (s[i] === "\\")
102
+ i++;
103
+ i++;
104
+ }
105
+ }
106
+ else if (ch === "[") {
107
+ depth++;
108
+ }
109
+ else if (ch === "]") {
110
+ depth--;
111
+ if (depth === 0)
112
+ return s.slice(openIdx + 1, i);
113
+ }
114
+ }
115
+ return null;
116
+ }
117
+ /** A matcher written in JS source escapes regex backslashes (`\\.`); collapse one
118
+ * level so the extracted pattern is a usable regex (`\.`). */
119
+ function unescapeMatcher(s) {
120
+ return s.replace(/\\\\/g, "\\");
121
+ }
122
+ /** Every quoted string literal inside a region (handles `]`, `,`, escapes within). */
123
+ function extractStringLiterals(region) {
124
+ const out = [];
125
+ const re = /(["'`])((?:\\.|(?!\1)[\s\S])*?)\1/g;
126
+ let m;
127
+ while ((m = re.exec(region)) !== null)
128
+ out.push(unescapeMatcher(m[2]));
129
+ return out;
130
+ }
85
131
  export function parseMiddlewareMatchers(content) {
86
- // Normalize literal escape sequences that AI assistants may pass
87
- let normalized = content.replace(/\\n/g, "\n").replace(/\\t/g, "\t");
88
- // Strip block + line comments before pulling the matcher array. Real-world
89
- // middleware files carry JSDoc-style notes inline (dub's matcher block has
90
- // four bullet points); split-on-comma was swallowing those bullets into the
91
- // matcher list, breaking every downstream regex test.
92
- normalized = normalized.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "");
93
- const stringMatch = /matcher\s*:\s*"([^"]+)"/.exec(normalized);
94
- if (stringMatch)
95
- return [stringMatch[1]];
96
- const arrayMatch = /matcher\s*:\s*\[([^\]]+)\]/.exec(normalized);
97
- if (arrayMatch) {
98
- return arrayMatch[1]
99
- .split(",")
100
- .map(s => s.trim().replace(/^["']|["']$/g, ""))
101
- .filter(Boolean);
132
+ const normalized = stripComments(content);
133
+ // Array form: matcher: [ ... ] — bound the array string-aware so a catch-all
134
+ // pattern containing `]`/`,` (e.g. Clerk's `[^?]`) isn't truncated.
135
+ const arrM = /matcher\s*:\s*\[/.exec(normalized);
136
+ if (arrM) {
137
+ const openIdx = normalized.indexOf("[", arrM.index);
138
+ const inner = bracketInner(normalized, openIdx);
139
+ if (inner !== null) {
140
+ const lits = extractStringLiterals(inner);
141
+ if (lits.length)
142
+ return lits;
143
+ }
102
144
  }
145
+ // String form: matcher: "..."
146
+ const strM = /matcher\s*:\s*(["'`])((?:\\.|(?!\1).)*)\1/.exec(normalized);
147
+ if (strM)
148
+ return [unescapeMatcher(strM[2])];
103
149
  return [];
104
150
  }
105
151
  /**
106
- * Convert a Next.js matcher pattern to a regex.
107
- * Handles :path* and :param patterns.
152
+ * Clerk-style protect lists: `createRouteMatcher([...])`. When present these are the
153
+ * precise routes the middleware enforces auth on — more accurate than config.matcher
154
+ * (which only says where the middleware *runs*), so a sensitive route outside the
155
+ * protect list is correctly still reported as unprotected.
156
+ */
157
+ export function parseProtectedRouteMatchers(content) {
158
+ const normalized = stripComments(content);
159
+ const out = [];
160
+ const callRe = /createRouteMatcher\s*\(\s*\[/g;
161
+ let m;
162
+ while ((m = callRe.exec(normalized)) !== null) {
163
+ const openIdx = normalized.indexOf("[", m.index);
164
+ const inner = bracketInner(normalized, openIdx);
165
+ if (inner !== null)
166
+ out.push(...extractStringLiterals(inner));
167
+ }
168
+ return out;
169
+ }
170
+ /**
171
+ * Convert a Next.js matcher to a regex. Path-style matchers (`/x/:id`, `/y/:path*`)
172
+ * get token conversion; regex-style matchers (Clerk catch-all, `(.*)`, char classes)
173
+ * are used raw. Tries the likely form first, falls back to the other, and NEVER throws
174
+ * on a malformed pattern (returns null, which callers skip).
108
175
  */
109
176
  function matcherToRegex(pattern) {
110
- const regexStr = pattern
111
- .replace(/\/:[\w]+\*/g, "(?:/.*)?")
112
- .replace(/:[\w]+/g, "[^/]+");
113
- return new RegExp("^" + regexStr + "$");
177
+ const pathConvert = (p) => p.replace(/\/:[\w]+\*/g, "(?:/.*)?").replace(/:[\w]+/g, "[^/]+");
178
+ const looksRegex = /[(|]|\.\*|\\[dwsDWS]|\[\^?/.test(pattern);
179
+ const candidates = looksRegex ? [pattern, pathConvert(pattern)] : [pathConvert(pattern), pattern];
180
+ for (const c of candidates) {
181
+ try {
182
+ return new RegExp("^" + c + "$");
183
+ }
184
+ catch { /* try next form */ }
185
+ }
186
+ return null;
114
187
  }
115
188
  /**
116
189
  * Check if a route URL path matches any of the middleware matchers.
@@ -121,7 +194,7 @@ export function routeMatchesMatcher(urlPath, matchers) {
121
194
  return true;
122
195
  for (const pattern of matchers) {
123
196
  const regex = matcherToRegex(pattern);
124
- if (regex.test(urlPath))
197
+ if (regex && regex.test(urlPath))
125
198
  return true;
126
199
  }
127
200
  return false;
@@ -154,8 +227,19 @@ function hasAuthGuard(code) {
154
227
  */
155
228
  export function analyzeAuthCoverage(routeFiles, middlewareContent, layoutFiles, authExceptions) {
156
229
  const routes = enumerateRoutes(routeFiles);
157
- const matchers = parseMiddlewareMatchers(middlewareContent);
158
230
  const hasMiddleware = middlewareContent.length > 0;
231
+ // Default-lenient: a middleware with a matcher counts as protection — EXCEPT when it
232
+ // is recognizably a non-auth middleware (i18n / analytics) with no auth signal, which
233
+ // must not mark routes protected (that would hide genuinely unprotected routes).
234
+ const hasAuthSignal = hasAuthGuard(middlewareContent) ||
235
+ /\b(?:clerkMiddleware|authMiddleware|withAuth|createRouteMatcher|NextAuth|auth0|betterAuth|supabaseMiddleware|updateSession|createServerClient|getToken)\b/.test(middlewareContent) ||
236
+ /auth\s*\.\s*protect\s*\(/.test(middlewareContent);
237
+ const isNonAuthMiddleware = /\b(?:next-intl|createI18nMiddleware|next-international|paraglide|@vercel\/analytics|posthog)\b/.test(middlewareContent)
238
+ || /from\s+["']next-intl\/middleware["']/.test(middlewareContent);
239
+ const middlewareCountsAsAuth = hasMiddleware && (hasAuthSignal || !isNonAuthMiddleware);
240
+ // Prefer the precise Clerk protect list; fall back to where the (auth) middleware runs.
241
+ const protectMatchers = parseProtectedRouteMatchers(middlewareContent);
242
+ const coverageMatchers = protectMatchers.length ? protectMatchers : parseMiddlewareMatchers(middlewareContent);
159
243
  // Map file content by path for auth detection
160
244
  const contentByPath = new Map();
161
245
  for (const f of routeFiles)
@@ -167,9 +251,9 @@ export function analyzeAuthCoverage(routeFiles, middlewareContent, layoutFiles,
167
251
  route.hasAuthGuard = hasAuthGuard(content);
168
252
  if (route.hasAuthGuard)
169
253
  route.protectionSource = "auth-guard";
170
- // Middleware coverage
171
- if (hasMiddleware) {
172
- route.middlewareCovered = routeMatchesMatcher(route.urlPath, matchers);
254
+ // Middleware coverage (skipped for recognizably non-auth middleware)
255
+ if (middlewareCountsAsAuth) {
256
+ route.middlewareCovered = routeMatchesMatcher(route.urlPath, coverageMatchers);
173
257
  if (route.middlewareCovered) {
174
258
  middlewareCoveredCount++;
175
259
  if (route.protectionSource === "none")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "guardvibe",
3
- "version": "3.7.0",
3
+ "version": "3.8.0",
4
4
  "mcpName": "io.github.goklab/guardvibe",
5
5
  "description": "Security infrastructure your AI can't be — deterministic, current past your model's training cutoff, whole-repo-aware, author-independent. Security MCP for vibe coding. 441 rules, 37 tools, CLI + doctor. Host security, auth coverage mapping, LLM-powered deep scan (IDOR/business logic), taint analysis. 70 CVE rules refreshed daily from GHSA/OSV/CISA KEV — React Router 7 cluster, DOMPurify XSS, Better Auth bypass, Miasma @redhat-cloud-services compromise, Next.js May 2026 13-advisory cluster, Drizzle/MikroORM/Kysely SQL injection, Axios proxy-auth redirect leak, Hono setCookie attribute injection, Clerk SSRF, tRPC prototype pollution, @tanstack supply-chain, node-ipc protestware, OpenClaude sandbox bypass, plus the full AI-generated stack (Supabase, Stripe, Prisma, Hono, GraphQL, Convex, Turso, Uploadthing, AI SDK). 68 AI-native rules including OWASP MCP Top 10 tool-description prompt injection (VG1068), model-controlled sandbox-disable flag detection (VG1063), Session messenger exfil endpoint IOC (VG1075), and CI/CD supply-chain hardening (VG1070 npm --expect-provenance / --ignore-scripts enforcement).",
6
6
  "type": "module",