guardvibe 3.1.37 → 3.1.38

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,17 @@ 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.38] - 2026-06-07
9
+
10
+ ### Fixed — false-positive precision, verified one rule at a time (no rule-count change, 438 / 36)
11
+ Each claimed false positive was checked against the actual code in the real-world corpus; only genuine FP classes were narrowed, with an old-vs-new diff confirming zero true-positive loss (14 FPs removed corpus-wide, 0 TP lost).
12
+ - **VG434** (Drizzle) retargeted from the *safe* `sql\`${value}\`` tag (which parameterizes interpolations) to the real injection vector — `sql.raw()` interpolated into an executed query. Renamed to "Drizzle sql.raw() Injection".
13
+ - **VG514** (Docker Compose secret) no longer fires when the value is an env-var reference (`${VAR}` / `$VAR`); hardcoded literals still fire.
14
+ - **VG001** (hardcoded credentials) skips kebab-case slug values under an uppercase-led enum/constant name (e.g. `UserMissingPassword = "missing-password"`) and values explicitly marked as mock/placeholder (e.g. `"MOCK_DAILY_API_KEY"`).
15
+ - **VG139** (TLS verification disabled) is skipped in test files and no longer matches a `skipVerify = false` substring; real `rejectUnauthorized: false` in production code still fires (the prior "FP" claim was wrong — those are real vulnerabilities).
16
+
17
+ Gate green (build / lint / test / self-audit PASS / A / 0).
18
+
8
19
  ## [3.1.37] - 2026-06-07
9
20
 
10
21
  ### Added — taint + secret scanning on the `check` path (no rule-count change, 438 / 36)
@@ -127,7 +127,7 @@ export const advancedSecurityRules = [
127
127
  severity: "critical",
128
128
  owasp: "A02:2025 Cryptographic Failures",
129
129
  description: "TLS certificate verification is disabled, allowing man-in-the-middle attacks. All HTTPS connections become insecure.",
130
- pattern: /(?:NODE_TLS_REJECT_UNAUTHORIZED\s*=\s*["']0["']|rejectUnauthorized\s*:\s*false|verify\s*=\s*False|InsecureSkipVerify\s*:\s*true)/gi,
130
+ pattern: /(?:NODE_TLS_REJECT_UNAUTHORIZED\s*=\s*["']0["']|rejectUnauthorized\s*:\s*false|(?<![\w$])verify\s*=\s*False|InsecureSkipVerify\s*:\s*true)/gi,
131
131
  languages: ["javascript", "typescript", "python", "go"],
132
132
  fix: "Never disable TLS verification in production. Fix certificate issues instead.",
133
133
  fixCode: '// BAD: disables all TLS verification\n// process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";\n// const agent = new https.Agent({ rejectUnauthorized: false });\n\n// GOOD: fix the certificate issue\n// - Use valid certificates (Let\'s Encrypt)\n// - Add CA certificate to Node: --use-openssl-ca\n// - For self-signed dev certs: only disable in NODE_ENV=development',
@@ -41,13 +41,13 @@ export const databaseRules = [
41
41
  },
42
42
  {
43
43
  id: "VG434",
44
- name: "Drizzle Unsafe SQL Interpolation",
44
+ name: "Drizzle sql.raw() Injection",
45
45
  severity: "critical",
46
46
  owasp: "A03:2025 Injection",
47
- description: "Drizzle sql tagged template with direct variable interpolation. Use sql.placeholder() for safe parameterization.",
48
- pattern: /(?:db\.execute|db\.run|db\.get|db\.all)\s*\(\s*sql`[^`]*\$\{/g,
47
+ description: "sql.raw() interpolated into an executed Drizzle query bypasses parameter binding, enabling SQL injection. Drizzle's sql`${value}` tag binds interpolations safely — only sql.raw() escapes that.",
48
+ pattern: /(?:db\.execute|db\.run|db\.get|db\.all)\s*\(\s*sql`[^`]*\$\{\s*sql\.raw\b/g,
49
49
  languages: ["javascript", "typescript"],
50
- fix: "Use sql.placeholder() for dynamic values in Drizzle queries.",
50
+ fix: "Avoid sql.raw() with dynamic input. Use bound ${value} interpolation, or restrict raw fragments to a hardcoded allowlist of column/table identifiers.",
51
51
  fixCode: 'import { sql } from "drizzle-orm";\n\nconst result = await db.execute(\n sql`SELECT * FROM users WHERE id = ${sql.placeholder("id")}`,\n { id: userId }\n);',
52
52
  compliance: ["SOC2:CC7.1", "PCI-DSS:Req6.5.1"],
53
53
  },
@@ -451,7 +451,7 @@ export function analyzeCode(code, language, framework, filePath, configDir, rule
451
451
  // agent.get('/?q=' + sqlPayload) which match the regex but aren't database calls
452
452
  // - VG042/VG678: HTTP-response/security-header rules (tests don't serve to real users)
453
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);
454
- if (isTestFile && ["VG001", "VG003", "VG062", "VG010", "VG011", "VG012", "VG013", "VG014", "VG042", "VG100", "VG130", "VG678", "VG955", "VG133", "VG1021", "VG409", "VG148", "VG424", "VG137"].includes(rule.id))
454
+ if (isTestFile && ["VG001", "VG003", "VG062", "VG010", "VG011", "VG012", "VG013", "VG014", "VG042", "VG100", "VG130", "VG678", "VG955", "VG133", "VG1021", "VG409", "VG148", "VG424", "VG137", "VG139"].includes(rule.id))
455
455
  continue;
456
456
  // VG137 (Debug Endpoint Exposes System Information) also misfires on build/test config
457
457
  // files: a `<rootDir>/test/` mapper or a `/test` path string near `process.env` in
@@ -944,6 +944,24 @@ export function analyzeCode(code, language, framework, filePath, configDir, rule
944
944
  if (canonical(idValuePair[1]) === canonical(idValuePair[2]))
945
945
  continue;
946
946
  }
947
+ // Skip enum/constant members whose value is a kebab-case slug (lowercase words
948
+ // joined by hyphens, no digits) under an uppercase-led name — error codes like
949
+ // `UserMissingPassword = "missing-password"`. The uppercase-name requirement keeps a
950
+ // lowercase `password = "my-secret"` firing (only enum/const members get the pass).
951
+ // The value is captured with a flat (ReDoS-safe) char class, then validated by
952
+ // splitting on '-' rather than a nested-quantifier regex.
953
+ const slugPair = matchedLine.match(/\b([A-Za-z_][A-Za-z0-9_]*)\s*[:=]\s*["']([a-z][a-z-]*)["']/);
954
+ if (slugPair && /^[A-Z]/.test(slugPair[1])) {
955
+ const parts = slugPair[2].split("-");
956
+ const isKebabSlug = parts.length >= 2 && parts.every(p => p.length > 0 && /^[a-z]+$/.test(p));
957
+ if (isKebabSlug)
958
+ continue;
959
+ }
960
+ // Skip values explicitly marked as mock/placeholder — `DAILY_API_KEY = "MOCK_DAILY_API_KEY"`,
961
+ // `apiKey: "your-api-key-here"`. A value literally named as a placeholder is not a secret.
962
+ const valuePair = matchedLine.match(/[:=]\s*["']([^"'\n]{3,})["']/);
963
+ if (valuePair && /^(?:mock|example|sample|demo|fake|dummy|stub|placeholder|changeme|change-me|your[-_]|xxx|todo|replace[-_]?me)/i.test(valuePair[1]))
964
+ continue;
947
965
  // Skip SCREAMING_SNAKE error/status codes whose value is digits-only.
948
966
  // e.g. `INVALID_PASSWORD = "5020"` — error code, not a credential.
949
967
  if (/\b[A-Z][A-Z0-9_]*\s*=\s*["']\d+["']/.test(matchedLine))
@@ -958,6 +976,16 @@ export function analyzeCode(code, language, framework, filePath, configDir, rule
958
976
  && /^[A-Za-z][A-Za-z .,!?'’()-]*\s[A-Za-z .,!?'’()-]+$/.test(msgPair[2]))
959
977
  continue;
960
978
  }
979
+ // VG514 (Docker Compose Hardcoded Secret): the match spans the `environment:` block,
980
+ // so the flagged secret VALUE is the last `KEY: value` in match[0]. When that value is
981
+ // an env-var reference (`${VAR}` / `$VAR`) it is NOT hardcoded — the canonical secure
982
+ // pattern (compose reads it from .env). Hardcoded literals (`POSTGRES_PASSWORD=magical`)
983
+ // still fire.
984
+ if (rule.id === "VG514") {
985
+ const secretVal = match[0].match(/(?:SECRET|PASSWORD|TOKEN|KEY|CREDENTIAL)\w*\s*[=:]\s*["']?([^"'\s]+)/i);
986
+ if (secretVal && /^\$\{?\w+\}?$/.test(secretVal[1]))
987
+ continue;
988
+ }
961
989
  // VG1083 (JWT verification bypass): jwt.decode() is fine when used only to peek at a
962
990
  // token that is ALSO verified (decode-then-verify). Skip the decode branch when a real
963
991
  // signature verification exists in the file. (The none-algorithm branch always fires.)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "guardvibe",
3
- "version": "3.1.37",
3
+ "version": "3.1.38",
4
4
  "mcpName": "io.github.goklab/guardvibe",
5
5
  "description": "Security MCP for vibe coding. 438 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",