guardvibe 3.1.31 → 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,32 @@ 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
+
23
+ ## [3.1.32] - 2026-06-06
24
+
25
+ ### Added — 4 new CVE rules (429 → 433), sourced via `npm run intel`
26
+ First rules added through the new intel-gap workflow: the daily check surfaced these as uncovered HIGH/CRITICAL npm advisories, each was written + tested + passed `npm run gate`.
27
+ - **VG1076** vitest < 4.1.0 — UI/API server arbitrary file read & execute (CVE-2026-47429, GHSA-5xrq-8626-4rwp, critical)
28
+ - **VG1077** @vitest/browser 4.0.17–4.1.5 + 5.0.0-beta.0→beta.2 — inline-script XSS via unsanitized `otelCarrier` query param (CVE-2026-47428, GHSA-2h32-95rg-cppp, critical)
29
+ - **VG1078** liquidjs < 10.26.0 — remote code execution via attacker-influenced templates (CVE-2026-45618, GHSA-gf2q-c269-pqgc, critical)
30
+ - **VG1079** tinymce < 5.11.1 / 6.0.0→7.9.2 / 8.0.0→8.5.0 — stored/DOM XSS cluster incl. media-plugin `data-mce-object` injection (CVE-2026-47759/47760/47761/47762, high)
31
+
32
+ CVE-version intelligence count 63 → 67. Tests +24. Self-audit PASS A 100.
33
+
8
34
  ## [3.1.31] - 2026-06-06
9
35
 
10
36
  ### Added — daily intel-gap triage
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
  [![npm provenance](https://img.shields.io/badge/provenance-verified-brightgreen)](https://www.npmjs.com/package/guardvibe)
7
7
  [![codecov](https://codecov.io/gh/goklab/guardvibe/graph/badge.svg)](https://codecov.io/gh/goklab/guardvibe)
8
8
 
9
- **The security MCP built for vibe coding.** 429 security rules, 36 tools covering the entire AI-generated code journey — from first line to production deployment.
9
+ **The security MCP built for vibe coding.** 433 security rules, 36 tools covering the entire AI-generated code journey — from first line to production deployment.
10
10
 
11
11
  Works with **Claude Code, Cursor, Gemini CLI, Codex, VS Code (Copilot), Windsurf**, and any MCP-compatible coding agent.
12
12
 
@@ -14,11 +14,11 @@ Works with **Claude Code, Cursor, Gemini CLI, Codex, VS Code (Copilot), Windsurf
14
14
 
15
15
  Most security tools are built for enterprise security teams. GuardVibe is built for **you** — the developer using AI to build and ship web apps fast.
16
16
 
17
- - **429 security rules, 36 tools** purpose-built for the stacks AI agents generate
17
+ - **433 security rules, 36 tools** purpose-built for the stacks AI agents generate
18
18
  - **Zero setup friction** — `npx guardvibe` and you're scanning
19
19
  - **No account required** — runs 100% locally, no API keys, no cloud
20
20
  - **Understands your stack** — not generic SAST, but rules that know Next.js, Supabase, Stripe, Clerk, and the tools you actually use
21
- - **CVE version intelligence** — detects 63 known vulnerable package versions in package.json, refreshed every day from GHSA / OSV.dev / CISA KEV
21
+ - **CVE version intelligence** — detects 67 known vulnerable package versions in package.json, refreshed every day from GHSA / OSV.dev / CISA KEV
22
22
  - **AI agent & MCP security** — detects MCP server vulnerabilities, tool-description prompt injection (OWASP MCP Top 10), model-controlled sandbox-disable flags, excessive AI permissions, indirect prompt injection
23
23
  - **Auto-fix suggestions** — `fix_code` tool returns concrete patches and structured edits the AI agent can apply mechanically. Coverage: hardcoded credentials → env-var migration; public-prefix LLM keys (`NEXT_PUBLIC_/VITE_/EXPO_PUBLIC_/REACT_APP_`) → prefix removal; CORS wildcards → env allowlist; `dangerouslyAllowBrowser` flags → drop; sandbox bypass flags (`unsafe`/`noSandbox`/`allowEval`) → drop; agent loops → add `maxSteps`; raw-HTML React props → `<ReactMarkdown>`; missing auth checks → insert auth guard; SQL injection → parameterized queries; missing rate limiters / CSRF / security headers → snippet templates.
24
24
  - **Pre-commit hook** — block insecure code before it reaches your repo
@@ -49,10 +49,10 @@ GuardVibe is purpose-built for the AI coding workflow. Traditional tools are exc
49
49
  | AI/LLM security (prompt injection, MCP, tool abuse) | 68 rules | Experimental/None | None |
50
50
  | AI host security (CVE-2025-59536, CVE-2026-21852) | `guardvibe doctor` | Not supported | Not supported |
51
51
  | Auto-fix suggestions for AI agents | `fix_code` tool | CLI autofix | Not supported |
52
- | CVE version detection | 63 packages, refreshed daily | Extensive | Extensive |
52
+ | CVE version detection | 67 packages, refreshed daily | Extensive | Extensive |
53
53
  | Compliance mapping (SOC2, PCI-DSS, HIPAA) | Built-in | Paid tier | None |
54
54
  | SARIF CI/CD export | Yes | Yes | Limited |
55
- | Rule count | 429 (focused, 68 AI-native) | 5000+ (broad) | N/A |
55
+ | Rule count | 433 (focused, 68 AI-native) | 5000+ (broad) | N/A |
56
56
 
57
57
  **When to use GuardVibe:** You're building with AI agents and want security scanning integrated into your coding workflow — no dashboard, no account, no CI setup.
58
58
 
@@ -176,7 +176,7 @@ React Native, Expo — AsyncStorage secrets, deep link token exposure, hardcoded
176
176
  ### Firebase
177
177
  Firestore security rules, Firebase Admin SDK exposure, storage rules, custom token validation
178
178
 
179
- ### CVE Version Intelligence (63 CVEs, refreshed daily)
179
+ ### CVE Version Intelligence (67 CVEs, refreshed daily)
180
180
  **Frameworks:** Next.js (CVE-2024-34351, CVE-2024-46982, CVE-2025-29927, CVE-2026-23869, CVE-2026-44573 / 44574 / 44575 / 44578 / 44579 / 45109 May 2026 cluster), React + react-server-dom-* (CVE-2025-55182, CVE-2026-23870), Express, Hono pre-4.12.18 cluster, @vitejs/plugin-rsc, Strapi content-type-builder (CVE-2026-22599)
181
181
  **Auth:** Clerk middleware bypass (GHSA-vqx2), Clerk `has()` org/billing/reverification bypass (GHSA-w24r), Clerk `clerkFrontendApiProxy` SSRF (CVE-2026-34076), NextAuth.js (2 CVEs), jsonwebtoken
182
182
  **ORMs / SQL:** Drizzle SQL identifier injection (CVE-2026-39356) + Drizzle `sql.raw` interpolation (VG1073), MikroORM SQL injection (CVE-2026-44680), Prisma raw-query call-form, Kysely JSON-path traversal (CVE-2026-44635)
@@ -242,7 +242,7 @@ Malicious postinstall scripts, unpinned GitHub Actions, CI `npm` provenance / `-
242
242
 
243
243
  All scanning tools support `format: "json"` for machine-readable output.
244
244
 
245
- ## Security Rules (429 rules across 25 modules)
245
+ ## Security Rules (433 rules across 25 modules)
246
246
 
247
247
  | Category | Rules | Coverage |
248
248
  |----------|-------|----------|
@@ -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.31`) 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
  {
@@ -733,4 +733,52 @@ export const cveVersionRules = [
733
733
  fixCode: '// package.json\n"overrides": { "node-ipc": "^12.0.0" }\n\n// pnpm-only\n// "pnpm": { "overrides": { "node-ipc": "^12.0.0" } }\n\n// yarn classic / berry\n// "resolutions": { "node-ipc": "^12.0.0" }',
734
734
  compliance: ["SOC2:CC7.1", "SOC2:CC8.1", "PCI-DSS:Req6.2"],
735
735
  },
736
+ {
737
+ id: "VG1076",
738
+ name: "Vitest UI Server Arbitrary File Read/Execute (CVE-2026-47429 / GHSA-5xrq-8626-4rwp)",
739
+ severity: "critical",
740
+ owasp: "A01:2025 Broken Access Control",
741
+ description: "vitest before 4.1.0 ships a UI/API server that, when listening, lets an attacker on the same network (or via a browser request when the dev server is exposed) read and execute arbitrary files on the developer or CI host. Vitest is a near-ubiquitous test runner, so a vulnerable pin in devDependencies exposes any machine that runs `vitest --ui` or leaves the API server bound during CI.",
742
+ pattern: /["']vitest["']\s*:\s*["'](?:\^|~|>=?)?\s*(?:[0-3]\.\d+\.\d+|4\.0\.\d+)["']/g,
743
+ languages: ["json"],
744
+ fix: "Upgrade vitest to 4.1.0+: npm install -D vitest@latest. Never expose the Vitest UI/API server beyond localhost, and bind it to 127.0.0.1 in CI rather than 0.0.0.0.",
745
+ fixCode: '// package.json\n"vitest": "^4.1.0" // or latest\n\n// vitest.config — keep the API/UI local-only\nexport default { test: { api: { host: "127.0.0.1" } } };',
746
+ compliance: ["SOC2:CC7.1", "PCI-DSS:Req6.2"],
747
+ },
748
+ {
749
+ id: "VG1077",
750
+ name: "Vitest Browser Mode Inline-Script XSS via otelCarrier (CVE-2026-47428 / GHSA-2h32-95rg-cppp)",
751
+ severity: "critical",
752
+ owasp: "A02:2025 Injection",
753
+ description: "@vitest/browser 4.0.17 through 4.1.5 (and the 5.0.0-beta.0 -> 5.0.0-beta.2 line) serves the `otelCarrier` query parameter back as an unsanitized inline script in browser mode. An attacker who can influence the URL of the running browser-mode session achieves cross-site scripting / arbitrary script execution in the test browser context, which on CI can pivot to the runner.",
754
+ pattern: /["']@vitest\/browser["']\s*:\s*["'](?:\^|~|>=?)?\s*(?:4\.0\.(?:1[7-9]|[2-9]\d)|4\.1\.[0-5]|5\.0\.0-beta\.[0-2])["']/g,
755
+ languages: ["json"],
756
+ fix: "Upgrade @vitest/browser to 4.1.6+ (stable) or 5.0.0-beta.3+ (beta): npm install -D @vitest/browser@latest. Keep browser-mode sessions on localhost only.",
757
+ fixCode: '// package.json\n"@vitest/browser": "^4.1.6" // or latest',
758
+ compliance: ["SOC2:CC7.1", "PCI-DSS:Req6.5.7"],
759
+ },
760
+ {
761
+ id: "VG1078",
762
+ name: "LiquidJS Remote Code Execution (CVE-2026-45618 / GHSA-gf2q-c269-pqgc)",
763
+ severity: "critical",
764
+ owasp: "A03:2025 Injection",
765
+ description: "liquidjs before 10.26.0 is vulnerable to remote code execution when rendering templates whose content or context an attacker can influence. Apps that let users supply Liquid templates or template fragments (CMS themes, email builders, low-code field templating) can be driven to execute arbitrary code on the server.",
766
+ pattern: /["']liquidjs["']\s*:\s*["'](?:\^|~|>=?)?\s*(?:[0-9]\.\d+\.\d+|10\.(?:[01]?\d|2[0-5])\.\d+)["']/g,
767
+ languages: ["json"],
768
+ fix: "Upgrade liquidjs to 10.26.0+: npm install liquidjs@latest. Never render user-supplied Liquid templates without sandboxing; treat template source as untrusted input.",
769
+ fixCode: '// package.json\n"liquidjs": "^10.26.0" // or latest',
770
+ compliance: ["SOC2:CC7.1", "PCI-DSS:Req6.5.1"],
771
+ },
772
+ {
773
+ id: "VG1079",
774
+ name: "TinyMCE XSS Cluster — media/object & content injection (CVE-2026-47759/47760/47761/47762)",
775
+ severity: "high",
776
+ owasp: "A02:2025 Injection",
777
+ description: "TinyMCE before 5.11.1, 6.0.0 -> 7.9.2, and 8.0.0 -> 8.5.0 carries a cluster of stored/DOM XSS vulnerabilities (GHSA-vg35-5wq7-3x7w, GHSA-v98h-vmpc-fpqv, GHSA-q742-qvgc-gc2f, GHSA-mh5m-5hw4-5c69) including media-plugin `data-mce-object` injection. Any app embedding the TinyMCE editor and rendering its output can be driven to execute attacker-supplied script in another user's browser.",
778
+ pattern: /["']tinymce["']\s*:\s*["'](?:\^|~|>=?)?\s*(?:[1-4]\.\d+\.\d+|5\.(?:[0-9]|10)\.\d+|5\.11\.0|6\.\d+\.\d+|7\.[0-8]\.\d+|7\.9\.[0-2]|8\.[0-4]\.\d+|8\.5\.0)["']/g,
779
+ languages: ["json"],
780
+ fix: "Upgrade tinymce to 7.9.3+ (v7) or 8.5.1+ (v8): npm install tinymce@latest. The 5.x line has no fix — migrate off it. Always sanitize TinyMCE output server-side before rendering it to other users.",
781
+ fixCode: '// package.json\n"tinymce": "^8.5.1" // or "^7.9.3" for v7\n\n// Sanitize editor output before persisting/rendering\nimport DOMPurify from "isomorphic-dompurify";\n' + 'const clean = DOMPurify.sanitize(editorHtml);',
782
+ compliance: ["SOC2:CC7.1", "PCI-DSS:Req6.5.7"],
783
+ },
736
784
  ];
@@ -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,8 +1,8 @@
1
1
  {
2
2
  "name": "guardvibe",
3
- "version": "3.1.31",
3
+ "version": "3.1.33",
4
4
  "mcpName": "io.github.goklab/guardvibe",
5
- "description": "Security MCP for vibe coding. 429 rules, 36 tools, CLI + doctor. Host security, auth coverage mapping, LLM-powered deep scan (IDOR/business logic), taint analysis. 63 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).",
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",
7
7
  "bin": {
8
8
  "guardvibe": "build/cli.js",