guardvibe 3.16.0 → 3.17.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,15 @@ 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.17.0] - 2026-06-09
9
+
10
+ ### Added — FAZ 3 part 3: AST constant-origin detection for VG126 (442 rules / 37 tools)
11
+ - **VG126 ("Dynamic RegExp from User Input") no longer fires when the argument is provably constant.** `regexpArgIsConstant` (AST) suppresses `new RegExp(x)` when `x` is: a string literal, a variable assigned from a string literal, the callback parameter of an iteration over a const string-array (incl. an imported SCREAMING_SNAKE_CASE list, by convention), or `someRegExp.source`/`.flags` (cloning a compiled RegExp). Minified bundles are skipped too. Anything not provably constant keeps firing.
12
+ - **Validated (clean stash diff): VG126 29 → 21; all 8 removed are confirmed non-user-input** — dub `detect-bot` ×2 (`UA_BOTS`/`REFERRER_BOTS` bot-pattern lists), a minified vendor bundle, and payload `deepCopyObject` ×5 (`new RegExp(cur.source, cur.flags)` RegExp clones). **0 true positives lost, 0 false positives added.** 10 RegExp tests.
13
+ - No rule or tool changes (442 / 37). FAZ 3 part 3 (reuses the part-1 AST engine).
14
+
15
+ Gate green (build / lint / test / self-audit PASS / A / 0).
16
+
8
17
  ## [3.16.0] - 2026-06-08
9
18
 
10
19
  ### Added — FAZ 3 part 2: AST BOLA ownership-guard detection for VG950 (442 rules / 37 tools)
@@ -16,3 +16,12 @@ export declare function paramReachesSink(code: string, filePath?: string): boole
16
16
  * firing — for a BOLA rule we prefer a false positive over hiding a real one.
17
17
  */
18
18
  export declare function bolaOwnershipGuarded(code: string, filePath: string | undefined, line: number): boolean;
19
+ /**
20
+ * True when the argument to a `new RegExp(...)` at `line` is PROVABLY a constant
21
+ * (a string literal, a variable assigned from a string literal, or the callback
22
+ * parameter of an iteration over a const string-array — the "bot list" pattern),
23
+ * so VG126 ("Dynamic RegExp from user input") is a false positive there. Returns
24
+ * false on any uncertainty so the rule keeps firing — a regex built from anything
25
+ * not provably constant stays flagged.
26
+ */
27
+ export declare function regexpArgIsConstant(code: string, filePath: string | undefined, line: number): boolean;
@@ -212,3 +212,93 @@ export function bolaOwnershipGuarded(code, filePath, line) {
212
212
  }
213
213
  return false;
214
214
  }
215
+ const ITER_METHODS = new Set(["map", "forEach", "some", "every", "filter", "find", "findIndex", "reduce", "flatMap"]);
216
+ /** First `const NAME = <initializer>` for NAME anywhere in the file (file-scope-ish). */
217
+ function findVarInit(ts, sf, name) {
218
+ let found;
219
+ const visit = (node) => {
220
+ if (!found && ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.name.text === name && node.initializer) {
221
+ found = node.initializer;
222
+ }
223
+ if (!found)
224
+ ts.forEachChild(node, visit);
225
+ };
226
+ visit(sf);
227
+ return found;
228
+ }
229
+ /** A non-empty array literal whose elements are all string/template literals (a const pattern list). */
230
+ function isConstStringArray(ts, sf, node) {
231
+ // SCREAMING_SNAKE_CASE identifier = constant by convention (often an imported list).
232
+ if (ts.isIdentifier(node) && /^[A-Z][A-Z0-9_]+$/.test(node.text))
233
+ return true;
234
+ let arr = node;
235
+ if (ts.isIdentifier(node))
236
+ arr = findVarInit(ts, sf, node.text);
237
+ if (arr && ts.isArrayLiteralExpression(arr)) {
238
+ return arr.elements.length > 0 &&
239
+ arr.elements.every(el => ts.isStringLiteral(el) || ts.isNoSubstitutionTemplateLiteral(el));
240
+ }
241
+ return false;
242
+ }
243
+ /**
244
+ * True when the argument to a `new RegExp(...)` at `line` is PROVABLY a constant
245
+ * (a string literal, a variable assigned from a string literal, or the callback
246
+ * parameter of an iteration over a const string-array — the "bot list" pattern),
247
+ * so VG126 ("Dynamic RegExp from user input") is a false positive there. Returns
248
+ * false on any uncertainty so the rule keeps firing — a regex built from anything
249
+ * not provably constant stays flagged.
250
+ */
251
+ export function regexpArgIsConstant(code, filePath, line) {
252
+ const ts = getTs();
253
+ if (!ts)
254
+ return false;
255
+ let sf;
256
+ try {
257
+ sf = ts.createSourceFile(filePath ?? "file.ts", code, ts.ScriptTarget.Latest, true, scriptKindFor(ts, filePath));
258
+ }
259
+ catch {
260
+ return false;
261
+ }
262
+ let target;
263
+ const visit = (node) => {
264
+ if (!target && ts.isNewExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "RegExp"
265
+ && node.arguments && node.arguments.length > 0) {
266
+ const startLine = sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
267
+ if (Math.abs(startLine - line) <= 1)
268
+ target = node;
269
+ }
270
+ if (!target)
271
+ ts.forEachChild(node, visit);
272
+ };
273
+ visit(sf);
274
+ if (!target || !target.arguments)
275
+ return false;
276
+ const arg = target.arguments[0];
277
+ if (ts.isStringLiteral(arg) || ts.isNoSubstitutionTemplateLiteral(arg))
278
+ return true;
279
+ // `new RegExp(x.source, x.flags)` — cloning an existing compiled RegExp, not user input.
280
+ if (ts.isPropertyAccessExpression(arg) && (arg.name.text === "source" || arg.name.text === "flags"))
281
+ return true;
282
+ if (!ts.isIdentifier(arg))
283
+ return false;
284
+ const argName = arg.text;
285
+ // (a) const argName = "literal"
286
+ const init = findVarInit(ts, sf, argName);
287
+ if (init && (ts.isStringLiteral(init) || ts.isNoSubstitutionTemplateLiteral(init)))
288
+ return true;
289
+ // (b) argName is the callback parameter of an iteration over a const string array.
290
+ let fn = target.parent;
291
+ while (fn && !((ts.isArrowFunction(fn) || ts.isFunctionExpression(fn))
292
+ && fn.parameters.some(p => ts.isIdentifier(p.name) && p.name.text === argName))) {
293
+ fn = fn.parent;
294
+ }
295
+ if (fn && (ts.isArrowFunction(fn) || ts.isFunctionExpression(fn))) {
296
+ const call = fn.parent;
297
+ if (call && ts.isCallExpression(call) && ts.isPropertyAccessExpression(call.expression)
298
+ && ITER_METHODS.has(call.expression.name.text)
299
+ && isConstStringArray(ts, sf, call.expression.expression)) {
300
+ return true;
301
+ }
302
+ }
303
+ return false;
304
+ }
@@ -4,7 +4,7 @@ import { loadConfig } from "../utils/config.js";
4
4
  import { loadIgnoreFile, isIgnored } from "../utils/ignore.js";
5
5
  import { securityBanner } from "../utils/banner.js";
6
6
  import { looksMinified } from "../utils/constants.js";
7
- import { paramReachesSink, bolaOwnershipGuarded } from "./ast-engine.js";
7
+ import { paramReachesSink, bolaOwnershipGuarded, regexpArgIsConstant } from "./ast-engine.js";
8
8
  /** CVE version-pin rule IDs are VG900-VG931 (and only these). Other VG9xx IDs
9
9
  * (VG983 Turso, VG990 SVG, VG998 OpenAI browser flag, etc.) are regular code-pattern
10
10
  * rules and should NOT be exempted from comment / string-literal skip logic. */
@@ -886,6 +886,12 @@ export function analyzeCode(code, language, framework, filePath, configDir, rule
886
886
  // `userId` that only appears in `select`, and sees a separate comparison statement.
887
887
  if (rule.id === "VG950" && filePath && bolaOwnershipGuarded(code, filePath, lineNumber))
888
888
  continue;
889
+ // VG126 (Dynamic RegExp from User Input): the regex matches `new RegExp(anyVar)`,
890
+ // but the var is often a provable constant — a string literal or the callback
891
+ // param iterating a const string array (the classic bot-UA / referrer-list pattern).
892
+ // Suppress only when AST proves the argument is constant; anything else keeps firing.
893
+ if (rule.id === "VG126" && (looksMinified(code) || (filePath && regexpArgIsConstant(code, filePath, lineNumber))))
894
+ continue;
889
895
  // VG202: skip when the FROM target matches a previous AS-alias in the same file.
890
896
  if (dockerStageAliases) {
891
897
  const target = match[0].replace(/^FROM\s+/i, "").split(/[:@\s]/)[0].toLowerCase();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "guardvibe",
3
- "version": "3.16.0",
3
+ "version": "3.17.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. 442 rules, 37 tools, CLI + doctor. Host security, auth coverage mapping, LLM-powered deep scan (IDOR/business logic), taint analysis. 71 CVE rules refreshed daily from GHSA/OSV/CISA KEV — Vite dev-server RCE, 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",