guardvibe 3.1.32 → 3.1.33

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,21 @@ 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.1.33] - 2026-06-07
9
+
10
+ ### Fixed — false-positive precision (no rule-count change, stays 433 / 36)
11
+ Surfaced by an end-to-end accuracy sweep across the labeled fixture set and the real-world corpus; each change has positive + negative tests and was cross-checked against an uncapped before/after diff (removal-only, zero real findings lost).
12
+ - **Engine:** multi-line `/* */` block comments are now stripped before matching (string-aware, scoped to C-style languages) so rules no longer fire on commented-out code; YAML/Python/shell/Dockerfile (which use `#`) are unaffected.
13
+ - **VG060** no longer flags MD5/SHA-1 used for file/build-artifact checksums (keeps real password-hashing).
14
+ - **VG1002** only flags query operators whose value is attacker-controlled (a static `{ $ne: true }` literal is skipped; `$where` built from a variable/concat/interpolation still fires).
15
+ - **VG123 / VG010 / taint** skip queries that are parameterized (`bind`/`replacements`/`$1`/`:name`) and whose only interpolation is a hash/encode helper.
16
+ - **VG951** recognizes ownership fields (`author`, `email`, `accountId`, …) in the where-clause.
17
+ - **VG138** ignores confirm-password (`password === cpassword`) and emptiness checks.
18
+ - **VG001** ignores UI/error-message string variables; **VG148**/**VG424** skip test (`.spec`) files.
19
+ - **VG013** renamed to "ORM/NoSQL query injection risk" with stack-aware remediation (Sequelize/TypeORM operator injection, not Mongo-only).
20
+
21
+ Tests 1820 → 1848. Self-audit PASS / A / 0. Determinism unchanged across the corpus.
22
+
8
23
  ## [3.1.32] - 2026-06-06
9
24
 
10
25
  ### Added — 4 new CVE rules (429 → 433), sourced via `npm run intel`
package/README.md CHANGED
@@ -457,7 +457,7 @@ If your AI agent cannot connect to GuardVibe:
457
457
 
458
458
  1. **Restart your IDE/agent.** MCP servers are started by the host application. After running `npx guardvibe init`, restart Claude Code, Cursor, or Gemini CLI for the config to take effect.
459
459
  2. **Check the config path.** Run `npx guardvibe init claude` again and verify the output shows the correct config file location (`.mcp.json` in your project root for Claude Code, `.cursor/mcp.json` for Cursor).
460
- 3. **Re-run `init` to upgrade.** When upgrading GuardVibe, re-run `npx guardvibe init claude` — `.mcp.json` is pinned to a specific version (e.g. `guardvibe@3.1.32`) at init time for fast deterministic startup. As of v3.1.2 the re-run also rewrites stale pins automatically (`Upgraded GuardVibe pin (3.1.27 → 3.1.28)`); since v3.1.27 the PostToolUse hook command is pinned to the same version (was `@latest`) and re-run upgrades a stale hook too. The same applies to `npx guardvibe hook install` and `npx guardvibe ci github` (since v3.1.3) — both are version-pinned at install/generate time and re-run to upgrade.
460
+ 3. **Re-run `init` to upgrade.** When upgrading GuardVibe, re-run `npx guardvibe init claude` — `.mcp.json` is pinned to a specific version (e.g. `guardvibe@3.1.33`) at init time for fast deterministic startup. As of v3.1.2 the re-run also rewrites stale pins automatically (`Upgraded GuardVibe pin (3.1.27 → 3.1.28)`); since v3.1.27 the PostToolUse hook command is pinned to the same version (was `@latest`) and re-run upgrades a stale hook too. The same applies to `npx guardvibe hook install` and `npx guardvibe ci github` (since v3.1.3) — both are version-pinned at install/generate time and re-run to upgrade.
461
461
  4. **Pre-3.1.1 users won't see the auto-update banner.** GuardVibe started writing a once-per-day "newer version available" notice to stderr in v3.1.1. If your install predates that, you'll never see it — run `npx -y guardvibe@latest init <host>` once to bake in the latest pin and start receiving banners on subsequent sessions.
462
462
  5. **Verify Node.js version.** GuardVibe requires Node.js >= 18.0.0. Check with `node --version`.
463
463
  6. **Check npx cache.** If you upgraded GuardVibe and the old version is cached, run `npx -y guardvibe@latest` to force the latest version.
@@ -20,7 +20,7 @@ export const apiSecurityRules = [
20
20
  severity: "critical",
21
21
  owasp: "API1:2023 Broken Object Level Authorization",
22
22
  description: "Delete or update operation uses user-supplied ID without verifying resource ownership. Any authenticated user can modify or delete other users' resources.",
23
- pattern: /(?:delete|update|destroy|remove)\s*\(\s*\{?\s*(?:where\s*:\s*\{)?\s*(?:id|_id)\s*:\s*(?:req\.(?:params|query|body)|params\.|args\.|input\.)(?:(?!userId|user_id|ownerId|owner_id|createdBy|created_by)[\s\S]){0,200}?\}/gi,
23
+ pattern: /(?:delete|update|destroy|remove)\s*\(\s*\{?\s*(?:where\s*:\s*\{)?\s*(?:id|_id)\s*:\s*(?:req\.(?:params|query|body)|params\.|args\.|input\.)(?:(?!userId|user_id|ownerId|owner_id|createdBy|created_by|author|authorId|author_id|email|userEmail|accountId|account_id|tenantId|tenant_id|orgId|org_id|organizationId)[\s\S]){0,200}?\}/gi,
24
24
  languages: ["javascript", "typescript"],
25
25
  fix: "Include the authenticated user's ID in the where clause to prevent unauthorized modifications.",
26
26
  fixCode: '// Scope mutations to the authenticated user\nconst { userId } = await auth();\nawait prisma.post.delete({\n where: { id: params.id, userId }, // ownership!\n});',
@@ -100,14 +100,14 @@ export const coreRules = [
100
100
  },
101
101
  {
102
102
  id: "VG013",
103
- name: "NoSQL injection risk",
103
+ name: "ORM/NoSQL query injection risk",
104
104
  severity: "high",
105
105
  owasp: "A02:2025 Injection",
106
- description: "User input passed directly to MongoDB/NoSQL query operators.",
106
+ description: "User input passed directly into an ORM/NoSQL query filter object — a MongoDB/Mongoose .find()/.findOne() or a SQL-ORM where clause (Sequelize .find({where}), TypeORM). When the value is an object instead of a scalar, an attacker can inject query operators (MongoDB $ne/$gt/$where, or Sequelize string-operator aliases like $gt/$ne in v4) to bypass authentication or filters. Express parses req.query via qs into nested objects, so query-param values reach the filter as objects unless coerced.",
107
107
  pattern: /(?:find|findOne|updateOne|deleteOne|aggregate)\s*\(\s*\{[^}]*(?:req\.|request\.|body\.|params\.)/gi,
108
108
  languages: ["javascript", "typescript"],
109
- fix: "Validate and sanitize input before using in queries. Use mongoose schema validation. Reject objects where strings are expected.",
110
- fixCode: "// Validate input type before query\nconst id = typeof req.params.id === 'string' ? req.params.id : '';\nawait collection.findOne({ _id: new ObjectId(id) });",
109
+ fix: "Coerce filter values to scalars before querying and reject objects where strings are expected: const id = typeof req.params.id === 'string' ? req.params.id : ''. For Mongoose use schema validation; for Sequelize wrap values (String(req.query.id)) and never spread raw req objects into a where clause.",
110
+ fixCode: "// Coerce to a scalar before using in any ORM/NoSQL query filter\nconst id = typeof req.params.id === 'string' ? req.params.id : '';\n// Mongoose: await collection.findOne({ _id: new ObjectId(id) });\n// Sequelize: await Model.findOne({ where: { id: String(req.query.id) } });",
111
111
  compliance: ["SOC2:CC7.1", "PCI-DSS:Req6.5.1"],
112
112
  },
113
113
  {
@@ -70,6 +70,77 @@ function isInComment(lines, lineNumber) {
70
70
  trimmed.startsWith("<!--") ||
71
71
  trimmed.startsWith("/*"));
72
72
  }
73
+ /**
74
+ * Compute the set of 1-based line numbers that fall inside a multi-line block
75
+ * comment (slash-star ... star-slash). `isInComment` only catches lines whose
76
+ * trimmed start is a comment marker, so a line like ` res.cookie(...)` sitting
77
+ * INSIDE a commented-out block (common in teaching repos that keep "Fix for X"
78
+ * demos inline) was scanned as live code — a false-positive class for VG100,
79
+ * VG042 and any other non-CVE rule. This is a string-aware lexer pass (skips
80
+ * markers that appear inside ' " ` strings and after a // line comment) so URLs
81
+ * (`http://`), division, and regex-ish literals don't spuriously open a block.
82
+ */
83
+ function computeBlockCommentLines(code) {
84
+ const inBlock = new Set();
85
+ let line = 1;
86
+ let state = "code";
87
+ for (let i = 0; i < code.length; i++) {
88
+ const c = code[i];
89
+ const c2 = i + 1 < code.length ? code[i + 1] : "";
90
+ if (c === "\n") {
91
+ line++;
92
+ if (state === "line")
93
+ state = "code";
94
+ continue;
95
+ }
96
+ switch (state) {
97
+ case "code":
98
+ if (c === "/" && c2 === "/") {
99
+ state = "line";
100
+ i++;
101
+ }
102
+ else if (c === "/" && c2 === "*") {
103
+ state = "block";
104
+ inBlock.add(line);
105
+ i++;
106
+ }
107
+ else if (c === "'")
108
+ state = "sq";
109
+ else if (c === '"')
110
+ state = "dq";
111
+ else if (c === "`")
112
+ state = "tpl";
113
+ break;
114
+ case "block":
115
+ inBlock.add(line);
116
+ if (c === "*" && c2 === "/") {
117
+ state = "code";
118
+ i++;
119
+ }
120
+ break;
121
+ case "sq":
122
+ if (c === "\\")
123
+ i++;
124
+ else if (c === "'")
125
+ state = "code";
126
+ break;
127
+ case "dq":
128
+ if (c === "\\")
129
+ i++;
130
+ else if (c === '"')
131
+ state = "code";
132
+ break;
133
+ case "tpl":
134
+ if (c === "\\")
135
+ i++;
136
+ else if (c === "`")
137
+ state = "code";
138
+ break;
139
+ // "line" state is exited at the newline handler above
140
+ }
141
+ }
142
+ return inBlock;
143
+ }
73
144
  /**
74
145
  * Check if a match is inside a multi-line string literal (template literal,
75
146
  * fixCode/description property, or string concatenation).
@@ -187,6 +258,7 @@ function hasAuthGuardPattern(code) {
187
258
  }
188
259
  // Pattern 3: function called with await that contains auth-like keywords in name
189
260
  // Broad catch: any function name containing auth/session/permission/guard/verify/protect
261
+ // guardvibe-ignore VG153 — dotted-identifier path matcher; each `\w+\.` segment is dot-anchored, so backtracking is linear, not catastrophic
190
262
  if (/await\s+(?:\w+\.)*\w*(?:auth|Auth|session|Session|permission|Permission|guard|Guard|verify|Verify|protect|Protect|check|Check|ensure|Ensure|require|Require|assert|Assert|authorize|Authorize)\w*\s*\(/i.test(code)) {
191
263
  return true;
192
264
  }
@@ -221,6 +293,7 @@ function hasRoleCheckPattern(code) {
221
293
  /(?:requireAdmin|requireRole|checkAdmin|isAdmin|verifyAdmin|assertAdmin)\s*\(/i.test(code))
222
294
  return true;
223
295
  // await requireAdmin() with error check pattern (naming-agnostic admin guard)
296
+ // guardvibe-ignore VG153 — dotted-identifier path matcher; dot-anchored segments make backtracking linear
224
297
  if (/await\s+(?:\w+\.)*\w*(?:Admin|admin)\w*\s*\([^)]*\)\s*;?\s*\n\s*if\s*\(/i.test(code))
225
298
  return true;
226
299
  return false;
@@ -346,6 +419,14 @@ export function analyzeCode(code, language, framework, filePath, configDir, rule
346
419
  if (customPattern.test(code))
347
420
  codeHasAuthGuard = true;
348
421
  }
422
+ // Line numbers inside multi-line /* */ block comments — computed once per file
423
+ // (string-aware) so the per-match comment skip can drop matches on commented-out
424
+ // code whose own line doesn't start with a comment marker. Gated to languages that
425
+ // actually use C-style /* */ comments — YAML/Python/shell/Dockerfile/TOML use #, so
426
+ // a `/*` there (e.g. a `# .../health/*` path glob in a k8s manifest) is NOT a comment
427
+ // opener and must not suppress real findings.
428
+ const usesCStyleBlockComments = language === "javascript" || language === "typescript" || language === "go";
429
+ const blockCommentLines = usesCStyleBlockComments && code.includes("/*") ? computeBlockCommentLines(code) : null;
349
430
  const effectiveRules = rules ?? owaspRules;
350
431
  for (const rule of effectiveRules) {
351
432
  if (!rule.languages.includes(language))
@@ -370,7 +451,7 @@ export function analyzeCode(code, language, framework, filePath, configDir, rule
370
451
  // agent.get('/?q=' + sqlPayload) which match the regex but aren't database calls
371
452
  // - VG042/VG678: HTTP-response/security-header rules (tests don't serve to real users)
372
453
  const isTestFile = filePath && /(?:\.(?:[\w-]+-)?(?:spec|test|e2e|stories|cy)\.(?:ts|tsx|js|jsx|mjs|cjs)$|_test\.go$|\/__tests__\/|\/__mocks__\/|\/tests?\/|\/cypress\/|\/playwright\/|\/dockertest\/|\/testutil\/|\/testhelpers?\/|\/testfixtures?\/)/i.test(filePath);
373
- if (isTestFile && ["VG001", "VG003", "VG062", "VG010", "VG011", "VG012", "VG013", "VG014", "VG042", "VG100", "VG130", "VG678", "VG955", "VG133", "VG1021", "VG409"].includes(rule.id))
454
+ if (isTestFile && ["VG001", "VG003", "VG062", "VG010", "VG011", "VG012", "VG013", "VG014", "VG042", "VG100", "VG130", "VG678", "VG955", "VG133", "VG1021", "VG409", "VG148", "VG424"].includes(rule.id))
374
455
  continue;
375
456
  // VG955 (Missing Pagination on List Endpoint): only fire on actual request-handling
376
457
  // surfaces — API routes, App Router `route.{ts,tsx}`, pages/api, or Server Actions.
@@ -792,6 +873,10 @@ export function analyzeCode(code, language, framework, filePath, configDir, rule
792
873
  const isMultiLineMatch = match[0].includes("\n");
793
874
  if (!isMultiLineMatch && isInComment(lines, lineNumber))
794
875
  continue;
876
+ // Single-line match sitting inside a /* ... */ block comment (its own line
877
+ // may not start with a comment marker) — commented-out dead code, skip.
878
+ if (!isMultiLineMatch && blockCommentLines?.has(lineNumber))
879
+ continue;
795
880
  if (isInsideStringLiteral(lines, lineNumber, code, match.index))
796
881
  continue;
797
882
  }
@@ -853,6 +938,71 @@ export function analyzeCode(code, language, framework, filePath, configDir, rule
853
938
  // e.g. `INVALID_PASSWORD = "5020"` — error code, not a credential.
854
939
  if (/\b[A-Z][A-Z0-9_]*\s*=\s*["']\d+["']/.test(matchedLine))
855
940
  continue;
941
+ // Skip UI/error message string variables: `invalidPasswordErrorMessage = "Invalid password"`.
942
+ // The identifier signals a user-facing message/label/error and the value is a prose phrase
943
+ // (letters + at least one space), not a credential. isHumanReadableString needs 4+ words;
944
+ // this catches shorter 2-3 word phrases when the name is clearly a message.
945
+ const msgPair = matchedLine.match(/\b([A-Za-z_][A-Za-z0-9_]*)\s*[:=]\s*["']([^"']{3,})["']/);
946
+ if (msgPair
947
+ && /(?:message|msg|error|\berr\b|label|title|hint|text|placeholder|description|tooltip|notice|warning|caption|heading|prompt|copy)/i.test(msgPair[1])
948
+ && /^[A-Za-z][A-Za-z .,!?'’()-]*\s[A-Za-z .,!?'’()-]+$/.test(msgPair[2]))
949
+ continue;
950
+ }
951
+ // VG138 (Plaintext Password Comparison): skip benign non-credential comparisons.
952
+ // (1) Confirm-password match: `req.body.password == req.body.cpassword` compares two
953
+ // user inputs from the same form, not a submission against a stored secret.
954
+ // (2) Emptiness/presence check: `password === ''` validates that a field was provided.
955
+ if (rule.id === "VG138") {
956
+ const matchedLine = lines[lineNumber - 1] ?? "";
957
+ if (/(?:cpassword|confirm[_]?password|password[_]?confirm(?:ation)?|password2|repeat[_]?password|retype[_]?password|verify[_]?password)/i.test(matchedLine))
958
+ continue;
959
+ if (/(?:password|passwd|pwd)\s*(?:===|!==|==|!=)\s*(['"])\1/i.test(matchedLine))
960
+ continue;
961
+ }
962
+ // VG1002 (MongoDB NoSQL Injection via Query Operators): a query operator only enables
963
+ // injection when its value is attacker-controlled. Skip ONLY when the operator's value is
964
+ // a pure literal (`{ $ne: true }`, `{ $gt: 5 }`, `{ $regex: "^a" }`) — a static internal
965
+ // filter. A value built from a variable, concatenation, or template interpolation
966
+ // (`$where: 'this.x == ' + id`, `$where: `...${id}``) is a real injection vector — keep it.
967
+ if (rule.id === "VG1002") {
968
+ const after = code.slice(match.index + match[0].length, match.index + match[0].length + 80);
969
+ const staticLiteral = /^\s*:\s*(?:true|false|null|-?\d+(?:\.\d+)?|'[^'`$+]*'|"[^"`$+]*")\s*[},\]]/.test(after);
970
+ if (staticLiteral)
971
+ continue;
972
+ }
973
+ // VG060 (Weak password hashing): MD5/SHA-1 have legitimate non-credential uses — file/
974
+ // build-artifact checksums, ETags, cache keys, content integrity. Skip when the context
975
+ // is clearly a checksum/digest-of-bytes (or a build-tool config) and not a password.
976
+ if (rule.id === "VG060") {
977
+ const isBuildConfig = filePath ? /(?:^|\/)(?:Gruntfile|gulpfile|webpack\.config|rollup\.config|vite\.config|esbuild|metro\.config)\.[cm]?[jt]s$/i.test(filePath) : false;
978
+ const start = Math.max(0, lineNumber - 5);
979
+ const window = lines.slice(start, lineNumber + 4).join("\n");
980
+ // NB: do NOT treat `.update(data)` / `.update(content)` as a checksum signal — `data`
981
+ // is too generic and `hash(data)` is exactly how weak password hashing looks. Require a
982
+ // file/byte-buffer or explicit checksum marker instead.
983
+ const looksLikeChecksum = /(?:readFileSync|createReadStream|\bBuffer\b|\.update\s*\(\s*(?:buffer|buf|fileBuffer)|fs\.read|\.md5\b|checksum|etag|integrity|cacheKey|cache[_-]?key|contentHash|fileHash|subresource)/i.test(window);
984
+ const looksLikePassword = /(?:password|passwd|\bpwd\b|credential|user\.pass|loginPass)/i.test(window);
985
+ if ((isBuildConfig || looksLikeChecksum) && !looksLikePassword)
986
+ continue;
987
+ }
988
+ // VG123 (SQL Injection via Template Literal) + VG010 (SQL injection): skip when the query
989
+ // is parameterized (sequelize bind/replacements or $1/:name placeholders) AND every ${...}
990
+ // interpolation is a safe transform (hash/encode/escape/number) — not raw user input. e.g.
991
+ // `query(`... email = $1 ... password = '${security.hash(req.body.password)}'`, { bind: [..] })`.
992
+ // VG010 is included because the VG010↔VG123 dedup makes VG010 take over the same line once
993
+ // VG123 is suppressed — without this the FP is just relabeled, not removed.
994
+ if (rule.id === "VG123" || rule.id === "VG010") {
995
+ const tplStart = code.indexOf("`", match.index);
996
+ if (tplStart !== -1) {
997
+ const tplEnd = code.indexOf("`", tplStart + 1);
998
+ const tpl = tplEnd !== -1 ? code.slice(tplStart + 1, tplEnd) : "";
999
+ const callCtx = code.slice(match.index, (tplEnd !== -1 ? tplEnd : match.index) + 200);
1000
+ const isParameterized = /\b(?:bind|replacements)\s*:/.test(callCtx) || /[=\s](?:\$\d+|:[a-zA-Z_]\w*)\b/.test(tpl);
1001
+ const interps = tpl.match(/\$\{[^}]*\}/g) || [];
1002
+ const allSafe = interps.length > 0 && interps.every(s => /\$\{\s*[\w$.]*(?:hash|sha\d*|md5|bcrypt|argon2?|hmac|digest|encode|escape|encodeURIComponent|toString|String|Number|parseInt|parseFloat)\b/i.test(s));
1003
+ if (isParameterized && allSafe)
1004
+ continue;
1005
+ }
856
1006
  }
857
1007
  // VG106 (Timing-Unsafe Secret Comparison): skip when one operand is a React useRef
858
1008
  // pattern (`*Ref.current`). Refs hold local component state, not user-provided input,
@@ -884,6 +1034,7 @@ export function analyzeCode(code, language, framework, filePath, configDir, rule
884
1034
  // - `z.enum(filterConfig.field.operators)` (TS `as const` config object) — when
885
1035
  // the file has any `as const` cast, treat nested property access as static
886
1036
  const matchedLine = lines[lineNumber - 1] ?? "";
1037
+ // guardvibe-ignore VG153 — dotted-identifier path matcher; dot-anchored segments make backtracking linear
887
1038
  if (/z\.enum\s*\(\s*[\w$]+(?:\.[\w$]+)+/.test(matchedLine)) {
888
1039
  if (/\.enumValues\b/.test(matchedLine))
889
1040
  continue;
@@ -59,6 +59,25 @@ const SANITIZERS = [
59
59
  /sanitizeHtml\s*\(/,
60
60
  /xss\s*\(/,
61
61
  ];
62
+ /**
63
+ * A SQL sink is NOT injectable when the query is parameterized (sequelize
64
+ * bind/replacements, or $1 / :name placeholders) AND every ${...} interpolation
65
+ * in the template is a safe transform (hash/encode/escape/number) rather than raw
66
+ * user input. e.g. sequelize.query(`... email = $1 ... password = '${security.hash(pw)}'`,
67
+ * { bind: [req.body.email] }) — the only interpolation is a fixed-charset hash, and
68
+ * the user value is bound. Without this, the inline-source loop reports req.body.*
69
+ * appearing inside the hash() call as a SQLi flow (false positive).
70
+ */
71
+ function isSafeParameterizedSqlSink(lines, sinkIdx) {
72
+ const ctx = lines.slice(sinkIdx, sinkIdx + 4).join("\n");
73
+ const parameterized = /\b(?:bind|replacements)\s*:/.test(ctx) || /[=\s](?:\$\d+|:[a-zA-Z_]\w*)\b/.test(ctx);
74
+ if (!parameterized)
75
+ return false;
76
+ const sinkLine = lines[sinkIdx] ?? "";
77
+ const tpl = (sinkLine.match(/`[^`]*`/) || [""])[0];
78
+ const interps = tpl.match(/\$\{[^}]*\}/g) || [];
79
+ return interps.every(s => /\$\{\s*[\w$.]*(?:hash|sha\d*|md5|bcrypt|argon2?|hmac|digest|encode|escape|encodeURIComponent|toString|String|Number|parseInt|parseFloat)\b/i.test(s));
80
+ }
62
81
  function extractAssignments(lines) {
63
82
  const assignments = [];
64
83
  const assignPattern = /(?:const|let|var)\s+([\w]+)\s*=\s*(.*)/;
@@ -131,6 +150,8 @@ export function analyzeTaint(code, language, filePath) {
131
150
  sink.pattern.lastIndex = 0;
132
151
  if (!sink.pattern.test(line))
133
152
  continue;
153
+ if (sink.type === "sql-injection" && isSafeParameterizedSqlSink(lines, i))
154
+ continue;
134
155
  for (const tVar of taintedVars) {
135
156
  if (line.includes(tVar.name)) {
136
157
  const chain = [];
@@ -160,6 +181,8 @@ export function analyzeTaint(code, language, filePath) {
160
181
  sink.pattern.lastIndex = 0;
161
182
  if (!sink.pattern.test(line))
162
183
  continue;
184
+ if (sink.type === "sql-injection" && isSafeParameterizedSqlSink(lines, i))
185
+ continue;
163
186
  for (const source of TAINT_SOURCES) {
164
187
  source.pattern.lastIndex = 0;
165
188
  if (source.pattern.test(line)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "guardvibe",
3
- "version": "3.1.32",
3
+ "version": "3.1.33",
4
4
  "mcpName": "io.github.goklab/guardvibe",
5
5
  "description": "Security MCP for vibe coding. 433 rules, 36 tools, CLI + doctor. Host security, auth coverage mapping, LLM-powered deep scan (IDOR/business logic), taint analysis. 67 CVE rules refreshed daily from GHSA/OSV/CISA KEV — 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",