guardvibe 3.15.0 → 3.16.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.16.0] - 2026-06-08
9
+
10
+ ### Added — FAZ 3 part 2: AST BOLA ownership-guard detection for VG950 (442 rules / 37 tools)
11
+ - **VG950 (BOLA — find-by-id without ownership) is now AST-aware.** It had no ownership handling at all (VG951 already excludes where-clause ownership). `bolaOwnershipGuarded` suppresses VG950 when the AST proves the query is ownership-guarded — EITHER an ownership field in the **WHERE clause** with a non-route-param value, OR a same-function **post-fetch ownership comparison** of the fetched resource against the session (e.g. `if (eventType.userId !== ctx.user.id) throw`).
12
+ - **Precise where the regex can't be:** it ignores a `userId` that only appears in `select` (a regex lookahead would suppress on that → false negative), it sees an ownership check that lives in a separate statement, and it refuses to count an ownership field whose value is itself a route param (still BOLA).
13
+ - **Validated (clean stash diff): VG950 22 → 15; all 7 removed are genuinely ownership-guarded** — 3 via where-clause ownership (dub rewards/oauth-apps/tokens), 4 via post-fetch comparison (cal schedule/ooo handlers, each with a real `…userId !== user.id` check). **0 true BOLA hidden (0 false negatives), 0 false positives introduced.** 10 new tests.
14
+ - No rule or tool changes (442 / 37). FAZ 3 part 2 (engine reused from part 1).
15
+
16
+ Gate green (build / lint / test / self-audit PASS / A / 0).
17
+
8
18
  ## [3.15.0] - 2026-06-08
9
19
 
10
20
  ### Added — FAZ 3 part 1: AST dataflow engine + precise VG406 (442 rules / 37 tools)
@@ -5,3 +5,14 @@
5
5
  * the rule keeps its prior behavior rather than silently hiding a finding.
6
6
  */
7
7
  export declare function paramReachesSink(code: string, filePath?: string): boolean;
8
+ /**
9
+ * BOLA ownership-guard detection for VG950 (find-by-user-id). Returns true (the
10
+ * query is ownership-guarded → suppress the finding) when EITHER:
11
+ * (1) the find call's WHERE clause (not select!) contains an ownership field
12
+ * whose value is not itself a route param, OR
13
+ * (2) the enclosing function performs a post-fetch ownership comparison of an
14
+ * ownership field against a session/user value.
15
+ * Returns false on uncertainty (no parser, no matching call) so the rule keeps
16
+ * firing — for a BOLA rule we prefer a false positive over hiding a real one.
17
+ */
18
+ export declare function bolaOwnershipGuarded(code: string, filePath: string | undefined, line: number): boolean;
@@ -136,3 +136,79 @@ export function paramReachesSink(code, filePath) {
136
136
  }
137
137
  return sinkArgs.some(args => refsTaint(args, tainted));
138
138
  }
139
+ const FIND_METHODS = new Set(["findUnique", "findFirst", "findById", "findOne", "getOne"]);
140
+ const OWNERSHIP_FIELDS = new Set([
141
+ "userId", "user_id", "ownerId", "owner_id", "authorId", "author_id", "createdById", "createdBy", "created_by",
142
+ "accountId", "account_id", "tenantId", "tenant_id", "orgId", "org_id", "organizationId",
143
+ "projectId", "project_id", "workspaceId", "workspace_id", "teamId", "team_id", "memberId", "member_id",
144
+ "programId", "customerId",
145
+ ]);
146
+ // A comparison of a fetched resource's ownership field, on a line that also references a session/user.
147
+ const OWNERSHIP_COMPARE = /\.\s*(?:userId|ownerId|authorId|createdById|teamId|workspaceId|orgId|organizationId|tenantId|memberId|accountId|projectId)\b\s*(?:===|!==|==|!=)/i;
148
+ const SESSION_REF = /\b(?:session|ctx|auth|currentUser|viewer|member|account|workspace|team|org|self|me|user)\b/i;
149
+ /**
150
+ * BOLA ownership-guard detection for VG950 (find-by-user-id). Returns true (the
151
+ * query is ownership-guarded → suppress the finding) when EITHER:
152
+ * (1) the find call's WHERE clause (not select!) contains an ownership field
153
+ * whose value is not itself a route param, OR
154
+ * (2) the enclosing function performs a post-fetch ownership comparison of an
155
+ * ownership field against a session/user value.
156
+ * Returns false on uncertainty (no parser, no matching call) so the rule keeps
157
+ * firing — for a BOLA rule we prefer a false positive over hiding a real one.
158
+ */
159
+ export function bolaOwnershipGuarded(code, filePath, line) {
160
+ const ts = getTs();
161
+ if (!ts)
162
+ return false;
163
+ let sf;
164
+ try {
165
+ sf = ts.createSourceFile(filePath ?? "file.ts", code, ts.ScriptTarget.Latest, true, scriptKindFor(ts, filePath));
166
+ }
167
+ catch {
168
+ return false;
169
+ }
170
+ let target;
171
+ const visit = (node) => {
172
+ if (!target && ts.isCallExpression(node)) {
173
+ const callee = node.expression;
174
+ const method = ts.isPropertyAccessExpression(callee) ? callee.name.text
175
+ : ts.isIdentifier(callee) ? callee.text : undefined;
176
+ if (method && FIND_METHODS.has(method)) {
177
+ const startLine = sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
178
+ if (Math.abs(startLine - line) <= 1)
179
+ target = node;
180
+ }
181
+ }
182
+ if (!target)
183
+ ts.forEachChild(node, visit);
184
+ };
185
+ visit(sf);
186
+ if (!target)
187
+ return false;
188
+ // (1) ownership field in the WHERE clause with a non-param value.
189
+ const arg0 = target.arguments[0];
190
+ if (arg0 && ts.isObjectLiteralExpression(arg0)) {
191
+ const whereProp = arg0.properties.find(p => ts.isPropertyAssignment(p) && p.name && ts.isIdentifier(p.name) && p.name.text === "where");
192
+ if (whereProp && ts.isPropertyAssignment(whereProp) && ts.isObjectLiteralExpression(whereProp.initializer)) {
193
+ for (const prop of whereProp.initializer.properties) {
194
+ const nm = prop.name && ts.isIdentifier(prop.name) ? prop.name.text : undefined;
195
+ if (nm && OWNERSHIP_FIELDS.has(nm)) {
196
+ const valText = ts.isPropertyAssignment(prop) ? prop.initializer.getText(sf) : nm;
197
+ if (!/\b(?:params|searchParams)\b/.test(valText))
198
+ return true;
199
+ }
200
+ }
201
+ }
202
+ }
203
+ // (2) post-fetch ownership comparison against a session/user value, in the same function.
204
+ let fn = target;
205
+ while (fn && !(ts.isFunctionDeclaration(fn) || ts.isFunctionExpression(fn) || ts.isArrowFunction(fn) || ts.isMethodDeclaration(fn))) {
206
+ fn = fn.parent;
207
+ }
208
+ const body = fn ? fn.getText(sf) : code;
209
+ for (const ln of body.split("\n")) {
210
+ if (OWNERSHIP_COMPARE.test(ln) && SESSION_REF.test(ln))
211
+ return true;
212
+ }
213
+ return false;
214
+ }
@@ -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 } from "./ast-engine.js";
7
+ import { paramReachesSink, bolaOwnershipGuarded } 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. */
@@ -880,6 +880,12 @@ export function analyzeCode(code, language, framework, filePath, configDir, rule
880
880
  const lineNumber = beforeMatch.split("\n").length;
881
881
  if (isLineSuppressed(suppressions, lineNumber, rule.id))
882
882
  continue;
883
+ // VG950 (BOLA find-by-id): suppress when AST proves the query is ownership-guarded —
884
+ // an ownership field in the WHERE clause (non-param value) or a post-fetch ownership
885
+ // comparison against the session. Precise where the regex can't be: it ignores a
886
+ // `userId` that only appears in `select`, and sees a separate comparison statement.
887
+ if (rule.id === "VG950" && filePath && bolaOwnershipGuarded(code, filePath, lineNumber))
888
+ continue;
883
889
  // VG202: skip when the FROM target matches a previous AS-alias in the same file.
884
890
  if (dockerStageAliases) {
885
891
  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.15.0",
3
+ "version": "3.16.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",