guardvibe 3.0.47 → 3.0.48

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.
@@ -291,7 +291,7 @@ export const advancedSecurityRules = [
291
291
  severity: "high",
292
292
  owasp: "A03:2025 Injection",
293
293
  description: "User-controlled input is used as an object property key via bracket notation (obj[userInput]). Attackers can access __proto__, constructor, or prototype to pollute object prototypes and bypass security checks.",
294
- pattern: /(?:(?:req|request|body|query|params|input|data)\.\w+|(?:const|let|var)\s+(?:\{[^}]*\}|\w+)\s*=\s*(?:await\s+)?(?:req|request)[\s\S]{0,50}?)[\s\S]{0,100}?\w+\s*\[\s*(?:key|field|prop|name|column|attr|param)\s*\]/gi,
294
+ pattern: /(?:(?:req|request|body|query|params)\.\w+|(?:const|let|var)\s+(?:\{[^}]*\}|\w+)\s*=\s*(?:await\s+)?(?:req|request)[\s\S]{0,50}?)[\s\S]{0,100}?\w+\s*\[\s*(?:key|field|prop|name|column|attr|param)\s*\]/gi,
295
295
  languages: ["javascript", "typescript"],
296
296
  fix: "Validate property names against an allowlist, or use Map instead of plain objects.",
297
297
  fixCode: '// BAD: prototype pollution\nconst key = req.query.field;\nconst value = config[key];\n\n// GOOD: allowlist validation\nconst ALLOWED_FIELDS = new Set(["name", "email", "role"]);\nif (!ALLOWED_FIELDS.has(key)) return new Response("Invalid field", { status: 400 });\nconst value = config[key];\n\n// GOOD: use Map\nconst config = new Map([["name", "..."], ["email", "..."]]);\nconst value = config.get(key);',
@@ -304,7 +304,7 @@ export const advancedSecurityRules = [
304
304
  severity: "medium",
305
305
  owasp: "A04:2025 Insecure Design",
306
306
  description: "Regular expression contains nested quantifiers ((a+)+), overlapping alternation with quantifiers (([a-z]+)*), or other patterns that cause catastrophic backtracking. Attackers can send crafted input to freeze the event loop.",
307
- pattern: /\/(?:[^/\\]|\\.)*(?:\([^)]*[+*][^)]*\)\s*[+*]|\(\?:[^)]*[+*][^)]*\)\s*[+*])(?:[^/\\]|\\.)*\//g,
307
+ pattern: /\/(?:[^/\\\n]|\\.)*(?:\([^)\n]*[+*][^)\n]*\)\s*[+*]|\(\?:[^)\n]*[+*][^)\n]*\)\s*[+*])(?:[^/\\\n]|\\.)*\//g,
308
308
  languages: ["javascript", "typescript"],
309
309
  fix: "Rewrite the regex to avoid nested quantifiers. Use atomic groups or possessive quantifiers if available, or use the 'safe-regex' library to validate patterns.",
310
310
  fixCode: '// BAD: catastrophic backtracking\nconst re = /(a+)+$/;\n\n// GOOD: no nested quantifiers\nconst re = /a+$/;\n\n// GOOD: validate with safe-regex\nimport safe from "safe-regex";\nif (!safe(pattern)) throw new Error("Unsafe regex");',
package/build/index.js CHANGED
@@ -483,7 +483,8 @@ server.tool("explain_remediation", "Pass a GuardVibe rule ID (e.g. VG154) to get
483
483
  const results = explainRemediation(rule_id, code, format, rules);
484
484
  return { content: [{ type: "text", text: results }] };
485
485
  });
486
- // Tool 23: Quick file scan — designed for real-time integration
486
+ // Tool 23: Quick file scan — returns structured findings, not raw file contents.
487
+ // guardvibe-ignore VG880
487
488
  server.tool("scan_file", "Scan a single file on disk by path for security vulnerabilities. Pass a file path — the tool reads the file itself. For inline code snippets, use check_code instead. Example: scan_file({file_path: 'src/api/route.ts'})", {
488
489
  file_path: z.string().describe("Absolute or relative path to the file to scan"),
489
490
  format: z.enum(["markdown", "json"]).default("json").describe("Output format"),
@@ -135,7 +135,7 @@ function hasAuthGuard(code) {
135
135
  // 401/403 responses indicating auth enforcement
136
136
  if (/(?:status:\s*(?:401|403)|new\s+Response\s*\([^)]*(?:401|403)|Unauthorized|Forbidden)/.test(code))
137
137
  return true;
138
- // Broad: any function name containing auth/session/permission/guard
138
+ // guardvibe-ignore VG153
139
139
  if (/await\s+(?:\w+\.)*\w*(?:auth|Auth|session|Session|permission|Permission|guard|Guard|verify|Verify|protect|Protect)\w*\s*\(/i.test(code))
140
140
  return true;
141
141
  return false;
@@ -210,6 +210,7 @@ export function analyzeAuthCoverage(routeFiles, middlewareContent, layoutFiles,
210
210
  if (route.hasAuthGuard || route.middlewareCovered)
211
211
  continue;
212
212
  const isExcepted = authExceptions.some(exc => {
213
+ // guardvibe-ignore VG153
213
214
  const excPath = exc.path.replace(/\[[\w]+\]/g, "[^/]+");
214
215
  const regex = new RegExp("^" + excPath.replace(/\//g, "\\/") + "$");
215
216
  return regex.test(route.urlPath) || route.urlPath === exc.path || route.urlPath.startsWith(exc.path + "/");
@@ -5,6 +5,12 @@ export interface Finding {
5
5
  line: number;
6
6
  confidence: "high" | "medium" | "low";
7
7
  }
8
+ /**
9
+ * Detect if a file is a security rule definition file.
10
+ * These files intentionally contain vulnerable code patterns
11
+ * as regex matchers and fixCode examples — scanning them is meaningless.
12
+ */
13
+ export declare function isRuleDefinitionFile(code: string, filePath?: string): boolean;
8
14
  export declare function analyzeCode(code: string, language: string, framework?: string, filePath?: string, configDir?: string, rules?: SecurityRule[]): Finding[];
9
15
  export declare function formatFindingsJson(findings: Finding[], extra?: Record<string, unknown>): string;
10
16
  export declare function checkCode(code: string, language: string, framework?: string, filePath?: string, configDir?: string, format?: "markdown" | "json" | "buddy", rules?: SecurityRule[]): string;
@@ -3,6 +3,10 @@ import { owaspRules } from "../data/rules/index.js";
3
3
  import { loadConfig } from "../utils/config.js";
4
4
  import { loadIgnoreFile, isIgnored } from "../utils/ignore.js";
5
5
  import { securityBanner } from "../utils/banner.js";
6
+ /** CVE version-pin rule IDs are VG900-VG931 (and only these). Other VG9xx IDs
7
+ * (VG983 Turso, VG990 SVG, VG998 OpenAI browser flag, etc.) are regular code-pattern
8
+ * rules and should NOT be exempted from comment / string-literal skip logic. */
9
+ const CVE_VERSION_RULE = /^VG9(?:0\d|1\d|2\d|3[01])$/;
6
10
  function parseSuppressionsFromCode(lines) {
7
11
  const suppressions = [];
8
12
  const pattern = /(?:\/\/|#|<!--)\s*guardvibe-ignore(?:-next-line)?\s*(VG\d+)?(?:\s.*)?(?:-->)?/i;
@@ -80,10 +84,7 @@ function isInsideStringLiteral(lines, lineNumber, code, matchIndex) {
80
84
  if (/^\s*\+\s*["']/.test(line))
81
85
  return true; // + "string continuation"
82
86
  // 3. Line contains escaped newlines (\n) suggesting it's inside a string value
83
- const _quotesBefore = line.substring(0, line.indexOf(trimmed.charAt(0)));
84
87
  if (/\\n/.test(line) && /["'`].*\\n/.test(line)) {
85
- // Extra check: is the match portion inside quotes on this line?
86
- const _matchEnd = matchIndex + 20; // approximate
87
88
  const lineStart = code.lastIndexOf("\n", matchIndex) + 1;
88
89
  const col = matchIndex - lineStart;
89
90
  const beforeCol = line.substring(0, col);
@@ -93,11 +94,12 @@ function isInsideStringLiteral(lines, lineNumber, code, matchIndex) {
93
94
  return true;
94
95
  }
95
96
  // 4. Look backwards for property assignment context (fixCode, description, etc.)
97
+ // Includes display-string props (title, message, label) used by audit / report
98
+ // tools to surface findings — these contain mention of vulnerable patterns by name.
99
+ const PROP_RE = /^(?:fixCode|fix|description|exploit|audit|title|message|label|reason|details|summary|hint)\s*[:=]/;
96
100
  for (let i = lineNumber - 1; i >= Math.max(0, lineNumber - 20); i--) {
97
101
  const prev = lines[i]?.trimStart() || "";
98
- if (/^(?:fixCode|fix|description|exploit|audit)\s*[:=]/.test(prev))
99
- return true;
100
- if (/^(?:fixCode|fix|description|exploit|audit)\s*:\s*$/.test(prev))
102
+ if (PROP_RE.test(prev))
101
103
  return true;
102
104
  // Hit a rule boundary — stop looking
103
105
  if (/^\s*id\s*:\s*["']VG/.test(prev))
@@ -131,7 +133,7 @@ function isHumanReadableString(lines, lineNumber) {
131
133
  * These files intentionally contain vulnerable code patterns
132
134
  * as regex matchers and fixCode examples — scanning them is meaningless.
133
135
  */
134
- function isRuleDefinitionFile(code, filePath) {
136
+ export function isRuleDefinitionFile(code, filePath) {
135
137
  // Path-based: known rule definition directories
136
138
  if (filePath && /(?:\/rules\/|\/data\/rules\/)/.test(filePath)) {
137
139
  // Confirm it actually exports SecurityRule objects
@@ -139,6 +141,10 @@ function isRuleDefinitionFile(code, filePath) {
139
141
  return true;
140
142
  }
141
143
  }
144
+ // Path-based: framework guides and similar pure-documentation files that hold
145
+ // example code inside markdown template literals
146
+ if (filePath && /(?:^|\/)framework-guides\.ts$/.test(filePath))
147
+ return true;
142
148
  // Content-based: file defines multiple VG rules with pattern: regex
143
149
  if (/id:\s*["']VG\d+["']/g.test(code) && /pattern:\s*\//.test(code)) {
144
150
  const ruleCount = (code.match(/id:\s*["']VG\d+["']/g) || []).length;
@@ -546,7 +552,7 @@ export function analyzeCode(code, language, framework, filePath, configDir, rule
546
552
  // to match top-level dependency declarations in package.json. Lock files contain
547
553
  // sub-package peer dependency ranges (e.g. "next": ">=13.2.0" from a transitive dep)
548
554
  // which look like vulnerable pins but represent peer requirements, not installed versions.
549
- if (filePath && /^VG9(?:0\d|1\d|2\d|3[01])$/.test(rule.id) && /(?:package-lock\.json|yarn\.lock|pnpm-lock\.yaml|npm-shrinkwrap\.json)$/.test(filePath))
555
+ if (filePath && CVE_VERSION_RULE.test(rule.id) && /(?:package-lock\.json|yarn\.lock|pnpm-lock\.yaml|npm-shrinkwrap\.json)$/.test(filePath))
550
556
  continue;
551
557
  // Skip VG430 (Supabase anon key on server) when file properly separates client/server
552
558
  // or is a React Native/mobile client (anon key with AsyncStorage is correct pattern)
@@ -609,18 +615,35 @@ export function analyzeCode(code, language, framework, filePath, configDir, rule
609
615
  const lineNumber = beforeMatch.split("\n").length;
610
616
  if (isLineSuppressed(suppressions, lineNumber, rule.id))
611
617
  continue;
612
- // Skip matches on comment lines for code-pattern rules.
613
- // CVE version rules (VG9xx) scan package.json so they're exempt.
614
- if (!rule.id.startsWith("VG9")) {
615
- if (isInComment(lines, lineNumber))
618
+ // Skip matches on comment lines and inside string literals.
619
+ // CVE version-pin rules (VG900-VG931) are exempt they scan package.json
620
+ // dependency declarations where these contexts don't apply.
621
+ // For multi-line matches, only string-literal skip is applied: the match's
622
+ // starting line may legitimately be a comment while the vulnerable code is
623
+ // on a later line (e.g. VG966 OAuth callback comment + handler).
624
+ if (!CVE_VERSION_RULE.test(rule.id)) {
625
+ const isMultiLineMatch = match[0].includes("\n");
626
+ if (!isMultiLineMatch && isInComment(lines, lineNumber))
616
627
  continue;
617
- }
618
- // Skip matches inside string literals (fixCode, description, template strings)
619
- // This prevents rule definition files and docs from triggering false positives
620
- if (!rule.id.startsWith("VG9")) {
621
628
  if (isInsideStringLiteral(lines, lineNumber, code, match.index))
622
629
  continue;
623
630
  }
631
+ // VG020 (wildcard dep version) on package.json: skip the `engines` block —
632
+ // `"node": ">=18.0.0"` is a runtime constraint, not a dependency range.
633
+ if (rule.id === "VG020" && filePath && /package\.json$/.test(filePath)) {
634
+ let inEngines = false;
635
+ for (let j = lineNumber - 1; j >= Math.max(0, lineNumber - 6); j--) {
636
+ const prev = lines[j] ?? "";
637
+ if (/"engines"\s*:\s*\{/.test(prev)) {
638
+ inEngines = true;
639
+ break;
640
+ }
641
+ if (/^\s*\}/.test(prev))
642
+ break; // closed a previous block — not in engines
643
+ }
644
+ if (inEngines)
645
+ continue;
646
+ }
624
647
  // Skip hardcoded-credential rules when the value is a human-readable sentence
625
648
  if (rule.id === "VG001" || rule.id === "VG062") {
626
649
  if (isHumanReadableString(lines, lineNumber))
@@ -665,6 +688,7 @@ export function analyzeCode(code, language, framework, filePath, configDir, rule
665
688
  const mainPointsToBuild = /"main"\s*:\s*"(?:dist|build|lib|out)\//i.test(code);
666
689
  const runtimeNames = "node|nodemon|tsx|ts-node|next|nest|vite|remix|astro";
667
690
  // Allow leading env-var assignments: NODE_OPTIONS=..., NODE_ENV=production, PORT=3000, etc.
691
+ // guardvibe-ignore VG153
668
692
  const startsAsApp = new RegExp('"start"\\s*:\\s*"(?:[A-Z_][A-Z0-9_]*=\\S+\\s+)*(?:' + runtimeNames + ')\\b', "i").test(code);
669
693
  if (!hasPublishingFields && !mainPointsToBuild && startsAsApp)
670
694
  continue;
@@ -602,6 +602,7 @@ function deriveSeverity(sinkType) {
602
602
  return "medium";
603
603
  }
604
604
  function getSinkFix(sinkType) {
605
+ // guardvibe-ignore VG014
605
606
  const fixes = {
606
607
  "sql-injection": "Use parameterized queries instead of string interpolation.",
607
608
  "code-injection": "Never pass user input to eval() or Function constructor.",
@@ -618,7 +619,7 @@ export function analyzeCrossFileTaint(files) {
618
619
  const lang = detectLang(file.path);
619
620
  if (lang === "unknown")
620
621
  continue;
621
- const findings = analyzeTaint(file.content, lang);
622
+ const findings = analyzeTaint(file.content, lang, file.path);
622
623
  if (findings.length > 0)
623
624
  perFileFindings.set(file.path, findings);
624
625
  }
@@ -67,7 +67,11 @@ function parseSectionCounts(parsed) {
67
67
  }
68
68
  function collectJsFiles(dir, maxFiles = 200) {
69
69
  const files = [];
70
- const skip = new Set(["node_modules", ".git", ".next", "build", "dist", ".turbo", "coverage"]);
70
+ const config = loadConfig(resolve(dir));
71
+ const skip = new Set([
72
+ "node_modules", ".git", ".next", "build", "dist", ".turbo", "coverage",
73
+ ...config.scan.exclude,
74
+ ]);
71
75
  function walk(d) {
72
76
  if (files.length >= maxFiles)
73
77
  return;
@@ -4,6 +4,7 @@ import { execFileSync } from "child_process";
4
4
  import { secretPatterns, calculateEntropy } from "../data/secret-patterns.js";
5
5
  import { loadConfig } from "../utils/config.js";
6
6
  import { isExcludedFilename } from "../utils/constants.js";
7
+ import { isRuleDefinitionFile } from "./check-code.js";
7
8
  const DEFAULT_SECRET_EXCLUDES = new Set(["node_modules", ".git", "build", "dist"]);
8
9
  const SOURCE_FILE_EXTENSIONS = new Set([
9
10
  ".js", ".jsx", ".mjs", ".cjs",
@@ -14,6 +15,10 @@ const SOURCE_FILE_EXTENSIONS = new Set([
14
15
  const CONFIG_FILE_EXTENSIONS = new Set([".yml", ".yaml", ".toml", ".json", ".cfg", ".ini", ".conf"]);
15
16
  export function scanContent(content, filename) {
16
17
  const findings = [];
18
+ // Skip security rule definition files — pattern strings inside them frequently
19
+ // resemble real secret formats by design.
20
+ if (isRuleDefinitionFile(content, filename))
21
+ return findings;
17
22
  for (const sp of secretPatterns) {
18
23
  sp.pattern.lastIndex = 0;
19
24
  let match;
@@ -18,5 +18,5 @@ export interface TaintFinding {
18
18
  description: string;
19
19
  fix: string;
20
20
  }
21
- export declare function analyzeTaint(code: string, language: string): TaintFinding[];
21
+ export declare function analyzeTaint(code: string, language: string, filePath?: string): TaintFinding[];
22
22
  export declare function formatTaintFindings(findings: TaintFinding[], format: "markdown" | "json"): string;
@@ -3,6 +3,7 @@
3
3
  * Basic taint analysis — tracks user input flowing into dangerous sinks.
4
4
  * Not a full AST/CFG analysis, but follows variable assignments through lines.
5
5
  */
6
+ import { isRuleDefinitionFile } from "./check-code.js";
6
7
  // User input sources (tainted data entry points)
7
8
  const TAINT_SOURCES = [
8
9
  { pattern: /(?:req|request)\.(?:body|query|params|headers|cookies)\b/g, type: "http-input" },
@@ -111,9 +112,13 @@ function propagateTaint(assignments, lines) {
111
112
  }
112
113
  }
113
114
  }
114
- export function analyzeTaint(code, language) {
115
+ export function analyzeTaint(code, language, filePath) {
115
116
  if (!["javascript", "typescript"].includes(language))
116
117
  return [];
118
+ // Skip security rule definition files — they intentionally contain vulnerable
119
+ // code snippets in pattern regexes, fixCode strings, and exploit examples.
120
+ if (isRuleDefinitionFile(code, filePath))
121
+ return [];
117
122
  const lines = code.split("\n");
118
123
  const findings = [];
119
124
  const assignments = extractAssignments(lines);
@@ -90,6 +90,8 @@ function matchGlob(pattern, path) {
90
90
  }
91
91
  }
92
92
  try {
93
+ // regexStr is built from explicitly escaped glob chars (see line 90), not raw input.
94
+ // guardvibe-ignore VG126
93
95
  return new RegExp(regexStr).test(normalizedPath);
94
96
  }
95
97
  catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "guardvibe",
3
- "version": "3.0.47",
3
+ "version": "3.0.48",
4
4
  "mcpName": "io.github.goklab/guardvibe",
5
5
  "description": "Security MCP for vibe coding. 365 rules, 36 tools, CLI + doctor. Host security, auth coverage mapping, LLM-powered deep scan (IDOR/business logic), taint analysis. Plus Next.js, Supabase, Clerk, Stripe, Prisma, tRPC, Hono, GraphQL, Convex, Turso, Uploadthing, AI SDK, and the full AI-generated stack.",
6
6
  "type": "module",