guardvibe 1.9.3 → 1.9.5

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
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from "module";
3
- import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync, unlinkSync } from "fs";
3
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync, unlinkSync, statSync } from "fs";
4
4
  import { join, dirname } from "path";
5
5
  import { homedir } from "os";
6
6
  const require = createRequire(import.meta.url);
@@ -110,6 +110,22 @@ function setupClaudeHooksAndGuide() {
110
110
  writeFileSync(claudeMdPath, `# Project Guidelines\n${guardvibeBlock}`, "utf-8");
111
111
  console.log(` [OK] Created CLAUDE.md with GuardVibe guidance`);
112
112
  }
113
+ // Add generated files to .gitignore so they don't get committed
114
+ addToGitignore([".claude.json", ".claude/", "CLAUDE.md"]);
115
+ }
116
+ function addToGitignore(entries) {
117
+ const gitignorePath = join(process.cwd(), ".gitignore");
118
+ let content = "";
119
+ try {
120
+ content = readFileSync(gitignorePath, "utf-8");
121
+ }
122
+ catch { /* no .gitignore yet */ }
123
+ const missing = entries.filter(e => !content.split("\n").some(line => line.trim() === e));
124
+ if (missing.length === 0)
125
+ return;
126
+ const block = `\n# GuardVibe / Claude Code (auto-added by guardvibe init)\n${missing.join("\n")}\n`;
127
+ writeFileSync(gitignorePath, content.trimEnd() + block, "utf-8");
128
+ console.log(` [OK] Added ${missing.join(", ")} to .gitignore`);
113
129
  }
114
130
  // ── Pre-commit hook ──────────────────────────────────────────────────
115
131
  const HOOK_SCRIPT = `#!/bin/sh
@@ -245,6 +261,36 @@ if (SCAN_SCRIPT_DETECTED) {
245
261
  else {
246
262
  main();
247
263
  }
264
+ /**
265
+ * Check if scan results should cause a non-zero exit based on --fail-on flag.
266
+ * Default: "critical" — only exit 1 on critical findings.
267
+ * Options: "critical", "high", "medium", "low", "none"
268
+ */
269
+ function shouldFail(result, failOn) {
270
+ if (failOn === "none")
271
+ return false;
272
+ const levels = {
273
+ low: ["critical", "high", "medium", "low"],
274
+ medium: ["critical", "high", "medium"],
275
+ high: ["critical", "high"],
276
+ critical: ["critical"],
277
+ };
278
+ const failLevels = levels[failOn] || levels.critical;
279
+ // Try JSON format first
280
+ try {
281
+ const parsed = JSON.parse(result);
282
+ if (parsed.summary) {
283
+ return failLevels.some(level => (parsed.summary[level] ?? 0) > 0);
284
+ }
285
+ if (parsed.findings) {
286
+ return parsed.findings.some((f) => failLevels.includes(f.severity));
287
+ }
288
+ }
289
+ catch { /* not JSON, try markdown tags */ }
290
+ // Markdown format: check for [SEVERITY] tags
291
+ const tags = failLevels.map(l => `[${l.toUpperCase()}]`);
292
+ return tags.some(tag => result.includes(tag));
293
+ }
248
294
  function parseArgs(args) {
249
295
  const flags = {};
250
296
  const positional = [];
@@ -288,8 +334,8 @@ async function runScan() {
288
334
  console.log(result);
289
335
  }
290
336
  if (format !== "sarif") {
291
- const hasBlocking = result.includes("[CRITICAL]") || result.includes("[HIGH]");
292
- if (hasBlocking)
337
+ const failOn = flags["fail-on"] ?? "high";
338
+ if (shouldFail(result, failOn))
293
339
  process.exit(1);
294
340
  }
295
341
  }
@@ -325,8 +371,8 @@ async function runDirectoryScan(targetPath, flags) {
325
371
  console.log(` [OK] Baseline saved to ${baselineFile}`);
326
372
  }
327
373
  if (format !== "sarif") {
328
- const hasBlocking = result.includes("[CRITICAL]") || result.includes("[HIGH]");
329
- if (hasBlocking)
374
+ const failOn = flags["fail-on"] ?? "critical";
375
+ if (shouldFail(result, failOn))
330
376
  process.exit(1);
331
377
  }
332
378
  }
@@ -405,9 +451,18 @@ async function runDiffScan(base, flags) {
405
451
  else {
406
452
  console.log(result);
407
453
  }
408
- const hasBlocking = allFindings.some(f => f.severity === "critical" || f.severity === "high");
409
- if (hasBlocking)
410
- process.exit(1);
454
+ const failOn = flags["fail-on"] ?? "critical";
455
+ if (failOn !== "none") {
456
+ const failLevels = {
457
+ low: ["critical", "high", "medium", "low"],
458
+ medium: ["critical", "high", "medium"],
459
+ high: ["critical", "high"],
460
+ critical: ["critical"],
461
+ };
462
+ const levels = failLevels[failOn] || failLevels.critical;
463
+ if (allFindings.some(f => levels.includes(f.severity)))
464
+ process.exit(1);
465
+ }
411
466
  }
412
467
  async function runFileCheck(filePath, flags) {
413
468
  const { checkCode } = await import("./tools/check-code.js");
@@ -443,8 +498,8 @@ async function runFileCheck(filePath, flags) {
443
498
  else {
444
499
  console.log(result);
445
500
  }
446
- const hasBlocking = result.includes("[CRITICAL]") || result.includes("[HIGH]");
447
- if (hasBlocking)
501
+ const failOn = flags["fail-on"] ?? "critical";
502
+ if (shouldFail(result, failOn))
448
503
  process.exit(1);
449
504
  }
450
505
  // ── Main CLI ─────────────────────────────────────────────────────────
@@ -468,6 +523,8 @@ function printUsage() {
468
523
  Options:
469
524
  --format <type> Output format: markdown (default), json, sarif
470
525
  --output <file> Write results to file instead of stdout
526
+ --fail-on <level> Exit 1 when findings at this level or above exist
527
+ critical (default) | high | medium | low | none
471
528
  --baseline <file> Compare against a previous scan JSON for fix tracking
472
529
  --save-baseline Save current scan as baseline (.guardvibe-baseline.json)
473
530
  --version, -V Print version and exit
@@ -561,7 +618,14 @@ async function main() {
561
618
  const cliArgs = args.slice(1);
562
619
  const { flags, positional } = parseArgs(cliArgs);
563
620
  const targetPath = positional[0] ?? ".";
564
- await runDirectoryScan(targetPath, flags);
621
+ // If target is a file (not directory), auto-redirect to check mode
622
+ if (targetPath !== "." && existsSync(targetPath) && !statSync(targetPath).isDirectory()) {
623
+ console.log(` [INFO] "${targetPath}" is a file. Running: guardvibe check ${targetPath}\n`);
624
+ await runFileCheck(targetPath, flags);
625
+ }
626
+ else {
627
+ await runDirectoryScan(targetPath, flags);
628
+ }
565
629
  }
566
630
  else if (command === "diff") {
567
631
  const cliArgs = args.slice(1);
@@ -163,6 +163,10 @@ export function analyzeCode(code, language, framework, filePath, configDir, rule
163
163
  const rateLimitRuleIds = new Set(["VG956", "VG030"]);
164
164
  if (isCronRoute && rateLimitRuleIds.has(rule.id))
165
165
  continue;
166
+ // Context-aware: skip rate limiting rules for webhook routes
167
+ // Webhooks are called by external services, not users — rate limiting is irrelevant
168
+ if (isWebhookRoute && rateLimitRuleIds.has(rule.id))
169
+ continue;
166
170
  // Context-aware: skip rate limiting rules for admin routes that have admin auth
167
171
  const isAdminRoute = filePath && /\/admin\//i.test(filePath);
168
172
  const hasAdminAuth = isAdminRoute && /(?:requireAdmin|adminOnly|orgRole|org:admin|isAdmin|checkRole|requireRole)/i.test(code);
@@ -126,7 +126,7 @@ export function checkProject(files, format = "markdown", rules) {
126
126
  actionItems.forEach((item, i) => {
127
127
  const fileCount = item.files.size;
128
128
  const fileLabel = fileCount === 1 ? "1 file" : `${fileCount} files`;
129
- lines.push(`${i + 1}. **[${item.rule.severity.toUpperCase()}] ${item.rule.name}** (${item.rule.id}) — ${item.count} occurrences in ${fileLabel}`, ` ${item.rule.fix}`, ``);
129
+ 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}`, ``);
130
130
  });
131
131
  }
132
132
  lines.push(`---`, ``);
@@ -220,7 +220,7 @@ export function scanDirectory(path, recursive = true, exclude = [], format = "ma
220
220
  actionItems.forEach((item, i) => {
221
221
  const fileCount = item.files.size;
222
222
  const fileLabel = fileCount === 1 ? "1 file" : `${fileCount} files`;
223
- lines.push(`${i + 1}. **[${item.rule.severity.toUpperCase()}] ${item.rule.name}** (${item.rule.id}) — ${item.count} occurrences in ${fileLabel}`, ` ${item.rule.fix}`, ``);
223
+ 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}`, ``);
224
224
  });
225
225
  lines.push(`---`, ``);
226
226
  for (const result of scanResults) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "guardvibe",
3
- "version": "1.9.3",
3
+ "version": "1.9.5",
4
4
  "description": "Security MCP for vibe coding. 277 rules, 24 tools for Next.js, Supabase, Clerk, Stripe, Prisma, tRPC, Hono, GraphQL, Convex, Turso, Uploadthing, AI SDK, and the full AI-generated stack.",
5
5
  "type": "module",
6
6
  "bin": {