guardvibe 2.9.1 → 2.9.4

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/cli.js CHANGED
File without changes
@@ -175,6 +175,37 @@ function hasRoleCheckPattern(code) {
175
175
  return true;
176
176
  return false;
177
177
  }
178
+ /**
179
+ * Known legitimate npm packages with suspicious-looking prefixes.
180
+ * These are widely-used packages that trigger VG872/VG873 false positives.
181
+ */
182
+ const LEGITIMATE_PREFIXED_PACKAGES = new Set([
183
+ "fast-glob", "fast-deep-equal", "fast-json-stable-stringify", "fast-json-stringify",
184
+ "fast-xml-parser", "fast-diff", "fast-levenshtein", "fast-redact", "fast-check",
185
+ "fast-uri", "fast-querystring", "fast-decode-uri-component", "fast-content-type-parse",
186
+ "safe-array-concat", "safe-stable-stringify", "safe-buffer", "safe-regex",
187
+ "safe-regex-test", "safe-push-apply",
188
+ "simple-git", "simple-update-notifier", "simple-swizzle", "simple-concat",
189
+ "native-promise-only", "native-url",
190
+ "pure-rand",
191
+ "clean-css", "clean-stack",
192
+ "modern-normalize", "modern-ahocorasick",
193
+ "enhanced-resolve",
194
+ "better-sqlite3", "better-opn",
195
+ "super-json",
196
+ "ultra-runner",
197
+ "core-js", "core-js-compat", "core-util-is", "core-js-pure",
198
+ "common-tags", "common-path-prefix",
199
+ "base-x", "base64-js",
200
+ "internal-slot", "internal-ip",
201
+ "shared-utils",
202
+ "original-url", "original-fs",
203
+ "secure-json-parse",
204
+ "native-run",
205
+ ]);
206
+ function isLegitimatePackage(name) {
207
+ return LEGITIMATE_PREFIXED_PACKAGES.has(name);
208
+ }
178
209
  /**
179
210
  * Calculate confidence level for a finding based on file context and match quality.
180
211
  */
@@ -214,6 +245,13 @@ export function analyzeCode(code, language, framework, filePath, configDir, rule
214
245
  // Pre-analyze: detect auth guards and role checks pattern-agnostically
215
246
  let codeHasAuthGuard = hasAuthGuardPattern(code);
216
247
  const codeHasRoleCheck = hasRoleCheckPattern(code);
248
+ // Pre-analyze: detect fix patterns to suppress false positives after remediation
249
+ const codeHasSanitization = /(?:DOMPurify\.sanitize|sanitize(?:Html|HTML)|xss\s*\(|purify\s*\(|escapeHtml|sanitizeHtml)\s*\(/i.test(code);
250
+ const codeHasUrlValidation = /(?:(?:validate|verify|check|safe|allowed)(?:Url|URL|Uri|URI)|(?:ALLOWED_(?:HOSTS|URLS|ORIGINS|DOMAINS))|(?:allowlist|whitelist|safelist)[\s\S]{0,50}?(?:includes|has|match))/i.test(code);
251
+ const codeHasUuidFilename = /(?:randomUUID|nanoid|uuidv4|v4\s*\(\)|crypto\.randomUUID)\s*\(/i.test(code);
252
+ const codeHasCronVerification = /(?:verify|validate|check)(?:Cron|Secret|Auth|Signature)\s*\(/i.test(code);
253
+ const isMigrationFile = filePath ? /(?:migrations?|supabase\/migrations|seeds?|fixtures)\//i.test(filePath) : false;
254
+ const isPeerDeps = /["']peerDependencies["']/i.test(code);
217
255
  // Config: check custom auth function names from .guardviberc
218
256
  if (!codeHasAuthGuard && config.authFunctions && config.authFunctions.length > 0) {
219
257
  const customPattern = new RegExp(`(?:${config.authFunctions.join("|")})\\s*\\(`, "i");
@@ -264,9 +302,25 @@ export function analyzeCode(code, language, framework, filePath, configDir, rule
264
302
  // Skip npm package rules (VG863/VG864/VG865): only apply to package.json files
265
303
  if ((rule.id === "VG863" || rule.id === "VG864" || rule.id === "VG865") && filePath && !filePath.endsWith("package.json"))
266
304
  continue;
267
- // Skip destructive DDL rules (VG540-VG542) in migration directories
268
- if (rule.id.startsWith("VG54") && filePath && /(?:migrations?|seeds?|fixtures)\//i.test(filePath))
305
+ // Skip destructive DDL rules (VG540-VG542) and view rules (VG439) in migration directories
306
+ if ((rule.id.startsWith("VG54") || rule.id === "VG439") && isMigrationFile)
307
+ continue;
308
+ // Skip innerHTML/XSS rules when DOMPurify or sanitization is present
309
+ if (codeHasSanitization && ["VG408", "VG012", "VG042"].includes(rule.id))
310
+ continue;
311
+ // Skip SSRF rules when URL validation/allowlist pattern is present
312
+ if (codeHasUrlValidation && ["VG120"].includes(rule.id))
313
+ continue;
314
+ // Skip filename rules when UUID-based filename generation is present
315
+ if (codeHasUuidFilename && rule.id === "VG993")
269
316
  continue;
317
+ // Skip cron secret rules when custom verification function is present
318
+ if (codeHasCronVerification && ["VG968", "VG503"].includes(rule.id))
319
+ continue;
320
+ // Skip CVE version rules in peerDependencies (ranges, not actual versions)
321
+ if (isPeerDeps && rule.id === "VG903")
322
+ continue;
323
+ // VG872/VG873 legitimate package filtering is handled at match level below
270
324
  // Skip server-only import rule (VG964) for files that are inherently server-only:
271
325
  // Route Handlers (app/api/), middleware, instrumentation, next.config,
272
326
  // lib/, utils/, tools/, server/, scripts/, CLI files, config files
@@ -324,6 +378,20 @@ export function analyzeCode(code, language, framework, filePath, configDir, rule
324
378
  if (isHumanReadableString(lines, lineNumber))
325
379
  continue;
326
380
  }
381
+ // Skip supply chain rules for known legitimate packages
382
+ if (["VG872", "VG873"].includes(rule.id)) {
383
+ const pkgMatch = /"([\w@/-]+)"/.exec(match[0]);
384
+ if (pkgMatch && isLegitimatePackage(pkgMatch[1]))
385
+ continue;
386
+ }
387
+ // Skip VG903 React version in peerDependencies sections
388
+ if (rule.id === "VG903") {
389
+ const beforeText = code.substring(0, match.index);
390
+ const lastPeer = beforeText.lastIndexOf("peerDependencies");
391
+ const lastDeps = Math.max(beforeText.lastIndexOf('"dependencies"'), beforeText.lastIndexOf('"devDependencies"'));
392
+ if (lastPeer > lastDeps)
393
+ continue;
394
+ }
327
395
  findings.push({
328
396
  rule: effectiveRule,
329
397
  match: match[0].substring(0, 80),
@@ -130,37 +130,25 @@ export function checkProject(files, format = "markdown", rules) {
130
130
  });
131
131
  }
132
132
  lines.push(`---`, ``);
133
- // Per-file details
133
+ // Per-file details (truncated to prevent MCP output overflow)
134
+ const MAX_DETAIL_FINDINGS = 30;
135
+ let detailCount = 0;
134
136
  for (const r of results) {
137
+ if (detailCount >= MAX_DETAIL_FINDINGS) {
138
+ const remaining = totalIssues - detailCount;
139
+ lines.push(``, `> **${remaining} more findings omitted.** Use \`check_code\` or \`scan_file\` on individual files for full details.`, ``);
140
+ break;
141
+ }
135
142
  const fileIssueCount = r.findings.length;
136
143
  lines.push(`## File: ${r.path} (${fileIssueCount} issues)`, ``);
137
- // Group findings by rule.id to match check-code formatting
138
- const grouped = new Map();
139
144
  for (const finding of r.findings) {
140
- const existing = grouped.get(finding.rule.id);
141
- if (existing) {
142
- existing.push(finding);
143
- }
144
- else {
145
- grouped.set(finding.rule.id, [finding]);
146
- }
147
- }
148
- const sortedGroups = Array.from(grouped.entries()).sort(([, aFindings], [, bFindings]) => {
149
- return (severityOrder[aFindings[0].rule.severity] ?? 99) - (severityOrder[bFindings[0].rule.severity] ?? 99);
150
- });
151
- for (const [, groupFindings] of sortedGroups) {
152
- const first = groupFindings[0];
153
- const icon = first.rule.severity.toUpperCase();
154
- if (groupFindings.length > 2) {
155
- const lineList = groupFindings.map((f) => `~${f.line}`).join(", ");
156
- lines.push(`## [${icon}] ${first.rule.name} (${first.rule.id})`, ``, `**OWASP:** ${first.rule.owasp}`, `**Occurrences:** ${groupFindings.length} (lines: ${lineList})`, `**Example match:** \`${first.match}\``, ``, first.rule.description, ``, `**Fix:** ${first.rule.fix}`, ...(first.rule.fixCode ? [``, `**Secure code:**`, `\`\`\``, first.rule.fixCode, `\`\`\``] : []), ``, `---`, ``);
157
- }
158
- else {
159
- for (const finding of groupFindings) {
160
- lines.push(`## [${icon}] ${finding.rule.name} (${finding.rule.id})`, ``, `**OWASP:** ${finding.rule.owasp}`, `**Line:** ~${finding.line}`, `**Match:** \`${finding.match}\``, ``, finding.rule.description, ``, `**Fix:** ${finding.rule.fix}`, ...(finding.rule.fixCode ? [``, `**Secure code:**`, `\`\`\``, finding.rule.fixCode, `\`\`\``] : []), ``, `---`, ``);
161
- }
162
- }
145
+ if (detailCount >= MAX_DETAIL_FINDINGS)
146
+ break;
147
+ const icon = finding.rule.severity.toUpperCase();
148
+ lines.push(`### [${icon}] ${finding.rule.name} (${finding.rule.id})`, `**Line:** ~${finding.line} | **Match:** \`${finding.match}\``, `**Fix:** ${finding.rule.fix}`, ``);
149
+ detailCount++;
163
150
  }
151
+ lines.push(`---`, ``);
164
152
  }
165
153
  }
166
154
  else {
@@ -99,17 +99,20 @@ export function complianceReport(path, framework, format = "markdown", rules, mo
99
99
  }
100
100
  lines.push(``);
101
101
  lines.push(`---`, ``);
102
+ const MAX_COMPLIANCE_FINDINGS = 50;
103
+ let complianceCount = 0;
102
104
  for (const [control, items] of sortedControls) {
105
+ if (complianceCount >= MAX_COMPLIANCE_FINDINGS) {
106
+ lines.push(``, `> **Additional findings omitted.** Use \`scan_file\` on individual files for full details.`, ``);
107
+ break;
108
+ }
103
109
  lines.push(`## ${control}`, ``);
104
110
  for (const item of items) {
111
+ if (complianceCount >= MAX_COMPLIANCE_FINDINGS)
112
+ break;
105
113
  const f = item.finding;
106
114
  lines.push(`- **[${f.rule.severity.toUpperCase()}]** ${f.rule.name} (${f.rule.id}) in \`${f.filePath}\`:${f.line}`);
107
- if (f.rule.exploit) {
108
- lines.push(` - **Exploit scenario:** ${f.rule.exploit}`);
109
- }
110
- if (f.rule.audit) {
111
- lines.push(` - **Audit evidence:** ${f.rule.audit}`);
112
- }
115
+ complianceCount++;
113
116
  }
114
117
  lines.push(``);
115
118
  }
@@ -127,8 +127,17 @@ export function scanDirectory(path, recursive = true, exclude = [], format = "ma
127
127
  // baseline file unreadable, skip comparison
128
128
  }
129
129
  }
130
+ // MCP output size limit — large projects can produce 300K+ characters which
131
+ // exceeds Claude Code's max allowed tokens for tool results.
132
+ const MAX_JSON_FINDINGS = 50;
133
+ const MAX_MD_FINDINGS = 30;
130
134
  if (format === "json") {
131
135
  const findingsWithFiles = scanResults.flatMap(r => r.findings.map(f => ({ ...f, rule: f.rule, file: r.path })));
136
+ // Sort by severity: critical first, then high, then medium
137
+ const severityRank = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
138
+ findingsWithFiles.sort((a, b) => (severityRank[a.rule.severity] ?? 4) - (severityRank[b.rule.severity] ?? 4));
139
+ const truncated = findingsWithFiles.length > MAX_JSON_FINDINGS;
140
+ const limitedFindings = findingsWithFiles.slice(0, MAX_JSON_FINDINGS);
132
141
  const baseJson = {
133
142
  summary: {
134
143
  total: allFindings.length,
@@ -136,12 +145,13 @@ export function scanDirectory(path, recursive = true, exclude = [], format = "ma
136
145
  low: allFindings.filter(f => f.rule.severity === "low").length,
137
146
  blocked: totalCritical > 0 || totalHigh > 0,
138
147
  grade, score,
148
+ ...(truncated ? { truncated: true, showing: MAX_JSON_FINDINGS, message: `Showing top ${MAX_JSON_FINDINGS} of ${allFindings.length} findings (sorted by severity). Use scan_file on individual files for full details.` } : {}),
139
149
  },
140
150
  metadata,
141
- findings: findingsWithFiles.map(f => ({
151
+ findings: limitedFindings.map(f => ({
142
152
  id: f.rule.id, name: f.rule.name, severity: f.rule.severity,
143
153
  owasp: f.rule.owasp, line: f.line, match: f.match, file: f.file,
144
- fix: f.rule.fix, fixCode: f.rule.fixCode, compliance: f.rule.compliance,
154
+ fix: f.rule.fix,
145
155
  })),
146
156
  baseline: findingsToBaseline(scanResults),
147
157
  };
@@ -224,11 +234,20 @@ export function scanDirectory(path, recursive = true, exclude = [], format = "ma
224
234
  lines.push(`${i + 1}. **[${item.rule.severity.toUpperCase()}] ${item.rule.name}** (${item.rule.id}) — ${item.count} ${item.count === 1 ? "occurrence" : "occurrences"} in ${fileLabel}`, ` ${item.rule.fix}`, ``);
225
235
  });
226
236
  lines.push(`---`, ``);
237
+ let findingsPrinted = 0;
227
238
  for (const result of scanResults) {
239
+ if (findingsPrinted >= MAX_MD_FINDINGS) {
240
+ const remaining = allFindings.length - findingsPrinted;
241
+ lines.push(``, `> **${remaining} more findings omitted.** Use \`scan_file\` on individual files for full details.`, ``);
242
+ break;
243
+ }
228
244
  lines.push(`## File: ${result.path} (${result.findings.length} issues)`, ``);
229
245
  for (const f of result.findings) {
246
+ if (findingsPrinted >= MAX_MD_FINDINGS)
247
+ break;
230
248
  const icon = f.rule.severity.toUpperCase();
231
- lines.push(`### [${icon}] ${f.rule.name} (${f.rule.id})`, `**Line:** ~${f.line} | **Match:** \`${f.match}\``, `${f.rule.description}`, `**Fix:** ${f.rule.fix}`, ...(f.rule.fixCode ? [``, `**Secure code:**`, `\`\`\``, f.rule.fixCode, `\`\`\``] : []), ``);
249
+ lines.push(`### [${icon}] ${f.rule.name} (${f.rule.id})`, `**Line:** ~${f.line} | **Match:** \`${f.match}\``, `**Fix:** ${f.rule.fix}`, ``);
250
+ findingsPrinted++;
232
251
  }
233
252
  lines.push(`---`, ``);
234
253
  }
@@ -237,9 +256,35 @@ export function scanDirectory(path, recursive = true, exclude = [], format = "ma
237
256
  lines.push(`## No Issues Found`, ``, `All files passed security checks.`);
238
257
  }
239
258
  if (skippedFiles.length > 0) {
240
- lines.push(``, `**Skipped files:**`);
241
- for (const s of skippedFiles)
242
- lines.push(`- ${s}`);
259
+ lines.push(``, `**Skipped files:** ${skippedFiles.length}`);
260
+ }
261
+ // ── Priority Summary Table (always at the end, visible in terminal) ──
262
+ if (totalIssues > 0) {
263
+ const severityOrder = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
264
+ const ruleStats = new Map();
265
+ for (const r of scanResults) {
266
+ for (const f of r.findings) {
267
+ const existing = ruleStats.get(f.rule.id);
268
+ if (existing) {
269
+ existing.files.add(r.path);
270
+ existing.count++;
271
+ }
272
+ else
273
+ ruleStats.set(f.rule.id, { rule: f.rule, files: new Set([r.path]), count: 1 });
274
+ }
275
+ }
276
+ const sorted = Array.from(ruleStats.values())
277
+ .sort((a, b) => (severityOrder[a.rule.severity] ?? 99) - (severityOrder[b.rule.severity] ?? 99))
278
+ .slice(0, 10);
279
+ lines.push(``, `---`, `## Priority Summary`, ``, `| # | Severity | Rule | Issue | Files | Count |`, `|---|----------|------|-------|-------|-------|`);
280
+ sorted.forEach((item, i) => {
281
+ const sev = item.rule.severity.toUpperCase();
282
+ lines.push(`| ${i + 1} | ${sev} | ${item.rule.id} | ${item.rule.name} | ${item.files.size} | ${item.count} |`);
283
+ });
284
+ if (ruleStats.size > 10) {
285
+ lines.push(``, `*+ ${ruleStats.size - 10} more rule types not shown*`);
286
+ }
287
+ lines.push(``);
243
288
  }
244
289
  lines.push(securityBanner({ total: totalIssues, critical: totalCritical, high: totalHigh, medium: totalMedium, score, grade, filesScanned: metadata.filesScanned }));
245
290
  return lines.join("\n");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "guardvibe",
3
- "version": "2.9.1",
3
+ "version": "2.9.4",
4
4
  "mcpName": "io.github.goklab/guardvibe",
5
5
  "description": "Security MCP for vibe coding. 334 rules, 31 tools, CLI + doctor. Host security: CVE-2025-59536 hook injection, CVE-2026-21852 base URL hijack, MCP config audit, AI host hardening. 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",