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.
- package/build/data/rules/advanced-security.js +2 -2
- package/build/index.js +2 -1
- package/build/tools/auth-coverage.js +2 -1
- package/build/tools/check-code.d.ts +6 -0
- package/build/tools/check-code.js +40 -16
- package/build/tools/cross-file-taint.js +2 -1
- package/build/tools/full-audit.js +5 -1
- package/build/tools/scan-secrets.js +5 -0
- package/build/tools/taint-analysis.d.ts +1 -1
- package/build/tools/taint-analysis.js +6 -1
- package/build/utils/ignore.js +2 -0
- package/package.json +1 -1
|
@@ -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
|
|
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: /\/(?:[
|
|
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 —
|
|
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
|
-
//
|
|
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 (
|
|
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 &&
|
|
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
|
|
613
|
-
// CVE version rules (
|
|
614
|
-
|
|
615
|
-
|
|
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
|
|
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);
|
package/build/utils/ignore.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "guardvibe",
|
|
3
|
-
"version": "3.0.
|
|
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",
|