guardvibe 3.0.46 → 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;
@@ -17,8 +21,22 @@ function parseSuppressionsFromCode(lines) {
17
21
  suppressions.push({ line: i + 2, ruleId });
18
22
  }
19
23
  else if (isCommentOnlyLine) {
24
+ // Comment-only line: suppress the comment's own line plus the next several
25
+ // lines, stopping early at a blank line or a new comment block. This makes
26
+ // suppress comments work for multi-line method chains (common Supabase / ORM
27
+ // builders span 3-5 lines from `.from(...)` through `.select(...).order(...)`).
20
28
  suppressions.push({ line: i + 1, ruleId });
21
- suppressions.push({ line: i + 2, ruleId });
29
+ for (let j = 1; j <= 5; j++) {
30
+ const nextLine = lines[i + j];
31
+ if (nextLine === undefined)
32
+ break;
33
+ const trimmed = nextLine.trim();
34
+ if (trimmed === "")
35
+ break;
36
+ if (/^\s*(?:\/\/|#|<!--)/.test(nextLine))
37
+ break;
38
+ suppressions.push({ line: i + 1 + j, ruleId });
39
+ }
22
40
  }
23
41
  else {
24
42
  suppressions.push({ line: i + 1, ruleId });
@@ -66,10 +84,7 @@ function isInsideStringLiteral(lines, lineNumber, code, matchIndex) {
66
84
  if (/^\s*\+\s*["']/.test(line))
67
85
  return true; // + "string continuation"
68
86
  // 3. Line contains escaped newlines (\n) suggesting it's inside a string value
69
- const _quotesBefore = line.substring(0, line.indexOf(trimmed.charAt(0)));
70
87
  if (/\\n/.test(line) && /["'`].*\\n/.test(line)) {
71
- // Extra check: is the match portion inside quotes on this line?
72
- const _matchEnd = matchIndex + 20; // approximate
73
88
  const lineStart = code.lastIndexOf("\n", matchIndex) + 1;
74
89
  const col = matchIndex - lineStart;
75
90
  const beforeCol = line.substring(0, col);
@@ -79,11 +94,12 @@ function isInsideStringLiteral(lines, lineNumber, code, matchIndex) {
79
94
  return true;
80
95
  }
81
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*[:=]/;
82
100
  for (let i = lineNumber - 1; i >= Math.max(0, lineNumber - 20); i--) {
83
101
  const prev = lines[i]?.trimStart() || "";
84
- if (/^(?:fixCode|fix|description|exploit|audit)\s*[:=]/.test(prev))
85
- return true;
86
- if (/^(?:fixCode|fix|description|exploit|audit)\s*:\s*$/.test(prev))
102
+ if (PROP_RE.test(prev))
87
103
  return true;
88
104
  // Hit a rule boundary — stop looking
89
105
  if (/^\s*id\s*:\s*["']VG/.test(prev))
@@ -117,7 +133,7 @@ function isHumanReadableString(lines, lineNumber) {
117
133
  * These files intentionally contain vulnerable code patterns
118
134
  * as regex matchers and fixCode examples — scanning them is meaningless.
119
135
  */
120
- function isRuleDefinitionFile(code, filePath) {
136
+ export function isRuleDefinitionFile(code, filePath) {
121
137
  // Path-based: known rule definition directories
122
138
  if (filePath && /(?:\/rules\/|\/data\/rules\/)/.test(filePath)) {
123
139
  // Confirm it actually exports SecurityRule objects
@@ -125,6 +141,10 @@ function isRuleDefinitionFile(code, filePath) {
125
141
  return true;
126
142
  }
127
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;
128
148
  // Content-based: file defines multiple VG rules with pattern: regex
129
149
  if (/id:\s*["']VG\d+["']/g.test(code) && /pattern:\s*\//.test(code)) {
130
150
  const ruleCount = (code.match(/id:\s*["']VG\d+["']/g) || []).length;
@@ -378,6 +398,12 @@ export function analyzeCode(code, language, framework, filePath, configDir, rule
378
398
  // for batch processing, not for serving to clients.
379
399
  if (rule.id === "VG955" && (isBatchScriptFile || isCronRoute))
380
400
  continue;
401
+ // Skip VG1006 (select('*') exposes columns) in batch/script paths — debug scripts,
402
+ // migrations, seeds, and fixtures run under service-role and write/log data
403
+ // server-side; '*' doesn't expose anything to a client. Application routes that
404
+ // do expose data still get flagged.
405
+ if (rule.id === "VG1006" && isBatchScriptFile)
406
+ continue;
381
407
  // Skip VG132 (Missing Request Body Size Limit) on Next.js route handlers and
382
408
  // pages/api endpoints — Next.js/Vercel apply a default 4.5MB body limit at the
383
409
  // platform layer, which is what the rule is checking for.
@@ -526,7 +552,7 @@ export function analyzeCode(code, language, framework, filePath, configDir, rule
526
552
  // to match top-level dependency declarations in package.json. Lock files contain
527
553
  // sub-package peer dependency ranges (e.g. "next": ">=13.2.0" from a transitive dep)
528
554
  // which look like vulnerable pins but represent peer requirements, not installed versions.
529
- 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))
530
556
  continue;
531
557
  // Skip VG430 (Supabase anon key on server) when file properly separates client/server
532
558
  // or is a React Native/mobile client (anon key with AsyncStorage is correct pattern)
@@ -589,18 +615,35 @@ export function analyzeCode(code, language, framework, filePath, configDir, rule
589
615
  const lineNumber = beforeMatch.split("\n").length;
590
616
  if (isLineSuppressed(suppressions, lineNumber, rule.id))
591
617
  continue;
592
- // Skip matches on comment lines for code-pattern rules.
593
- // CVE version rules (VG9xx) scan package.json so they're exempt.
594
- if (!rule.id.startsWith("VG9")) {
595
- 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))
596
627
  continue;
597
- }
598
- // Skip matches inside string literals (fixCode, description, template strings)
599
- // This prevents rule definition files and docs from triggering false positives
600
- if (!rule.id.startsWith("VG9")) {
601
628
  if (isInsideStringLiteral(lines, lineNumber, code, match.index))
602
629
  continue;
603
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
+ }
604
647
  // Skip hardcoded-credential rules when the value is a human-readable sentence
605
648
  if (rule.id === "VG001" || rule.id === "VG062") {
606
649
  if (isHumanReadableString(lines, lineNumber))
@@ -645,6 +688,7 @@ export function analyzeCode(code, language, framework, filePath, configDir, rule
645
688
  const mainPointsToBuild = /"main"\s*:\s*"(?:dist|build|lib|out)\//i.test(code);
646
689
  const runtimeNames = "node|nodemon|tsx|ts-node|next|nest|vite|remix|astro";
647
690
  // Allow leading env-var assignments: NODE_OPTIONS=..., NODE_ENV=production, PORT=3000, etc.
691
+ // guardvibe-ignore VG153
648
692
  const startsAsApp = new RegExp('"start"\\s*:\\s*"(?:[A-Z_][A-Z0-9_]*=\\S+\\s+)*(?:' + runtimeNames + ')\\b', "i").test(code);
649
693
  if (!hasPublishingFields && !mainPointsToBuild && startsAsApp)
650
694
  continue;
@@ -674,6 +718,16 @@ export function analyzeCode(code, language, framework, filePath, configDir, rule
674
718
  if (/\.(?:order|range|limit)\s*\(/i.test(matched))
675
719
  continue;
676
720
  }
721
+ // Skip VG1006 (select('*') exposes columns) for count-only queries —
722
+ // .select('*', { count: 'exact', head: true }) returns only a row count, never
723
+ // materializes rows, so '*' doesn't expose any columns. The rule's pattern
724
+ // truncates at the `*` quote, so we look at the next ~120 chars for the head
725
+ // option (typical select(...) options object fits in that window).
726
+ if (rule.id === "VG1006") {
727
+ const tail = code.substring(match.index, match.index + (match[0].length + 120));
728
+ if (/\bhead\s*:\s*true\b/i.test(tail))
729
+ continue;
730
+ }
677
731
  // Skip VG155 (CSRF) in Next.js App Router route handlers (app/.../route.{ts,tsx,js,jsx}).
678
732
  // App Router protects state-changing requests by default: SameSite=Lax cookies block
679
733
  // cross-site cookie attachment, and JSON Content-Type triggers CORS preflight. Bearer-token
@@ -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.46",
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",