guardvibe 1.9.4 → 1.9.6

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);
@@ -50,6 +50,9 @@ function setupPlatform(name) {
50
50
  }
51
51
  if (existing.mcpServers["guardvibe"]) {
52
52
  console.log(` [OK] GuardVibe already configured in ${platform.description}`);
53
+ // Still ensure CLAUDE.md and .gitignore are set up
54
+ if (name === "claude")
55
+ setupClaudeHooksAndGuide();
53
56
  return true;
54
57
  }
55
58
  existing.mcpServers["guardvibe"] = GUARDVIBE_MCP_CONFIG;
@@ -225,9 +228,6 @@ jobs:
225
228
  with:
226
229
  node-version: "22"
227
230
 
228
- - name: Install dependencies
229
- run: npm ci
230
-
231
231
  - name: Run GuardVibe security scan
232
232
  run: npx -y guardvibe-scan --format sarif --output guardvibe-results.sarif
233
233
 
@@ -523,8 +523,8 @@ function printUsage() {
523
523
  Options:
524
524
  --format <type> Output format: markdown (default), json, sarif
525
525
  --output <file> Write results to file instead of stdout
526
- --fail-on <level> Exit 1 when findings at this level exist
527
- Options: critical (default), high, medium, low, none
526
+ --fail-on <level> Exit 1 when findings at this level or above exist
527
+ critical (default) | high | medium | low | none
528
528
  --baseline <file> Compare against a previous scan JSON for fix tracking
529
529
  --save-baseline Save current scan as baseline (.guardvibe-baseline.json)
530
530
  --version, -V Print version and exit
@@ -618,7 +618,14 @@ async function main() {
618
618
  const cliArgs = args.slice(1);
619
619
  const { flags, positional } = parseArgs(cliArgs);
620
620
  const targetPath = positional[0] ?? ".";
621
- 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
+ }
622
629
  }
623
630
  else if (command === "diff") {
624
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);
@@ -340,6 +344,7 @@ export function formatFindingsJson(findings, extra) {
340
344
  return JSON.stringify({
341
345
  summary: {
342
346
  total: findings.length, critical, high, medium, low,
347
+ // blocked: true when critical or high findings exist (would fail --fail-on high)
343
348
  blocked: critical > 0 || high > 0,
344
349
  ...extra,
345
350
  },
@@ -362,19 +367,68 @@ export function checkCode(code, language, framework, filePath, configDir, format
362
367
  }
363
368
  function formatCleanReport(language, framework) {
364
369
  const ctx = framework ? ` (${framework})` : "";
370
+ const tips = getLanguageTips(language, framework);
365
371
  return [
366
372
  `# GuardVibe Security Report`,
367
373
  ``,
368
374
  `**Language:** ${language}${ctx}`,
369
375
  `**Status:** No security issues detected`,
370
376
  ``,
371
- `The code looks clean! Here are some general tips:`,
372
- `- Keep dependencies updated (\`npm audit\`)`,
373
- `- Validate all user input with schemas (zod, joi)`,
374
- `- Use environment variables for secrets`,
375
- `- Add rate limiting to API endpoints`,
377
+ `Tips for ${language}${ctx}:`,
378
+ ...tips.map(t => `- ${t}`),
376
379
  ].join("\n");
377
380
  }
381
+ function getLanguageTips(language, framework) {
382
+ if (framework === "nextjs" || framework === "next")
383
+ return [
384
+ "Use `server-only` imports in files with secrets or DB access",
385
+ "Validate Server Action inputs with zod schemas",
386
+ "Set `serverActions.allowedOrigins` in next.config",
387
+ "Add security headers via `headers()` in next.config",
388
+ ];
389
+ if (framework === "express" || framework === "fastify" || framework === "hono")
390
+ return [
391
+ "Add rate limiting middleware to auth and write endpoints",
392
+ "Use helmet() for security headers",
393
+ "Validate request body with zod or joi before processing",
394
+ "Never reflect user input in error responses",
395
+ ];
396
+ if (language === "python")
397
+ return [
398
+ "Use parameterized queries — never f-strings in SQL",
399
+ "Add `Depends(get_current_user)` to protected routes",
400
+ "Pin dependency versions in requirements.txt",
401
+ "Use `secrets.compare_digest()` for token comparison",
402
+ ];
403
+ if (language === "sql")
404
+ return [
405
+ "Use `SECURITY INVOKER` on views to respect RLS",
406
+ "Avoid `GRANT ALL` — use least-privilege permissions",
407
+ "Add `IF EXISTS` to destructive DDL for safety",
408
+ "Use parameterized queries in application code",
409
+ ];
410
+ if (language === "dockerfile")
411
+ return [
412
+ "Use specific image tags, never `latest`",
413
+ "Run as non-root user with `USER` directive",
414
+ "Use multi-stage builds to minimize attack surface",
415
+ "Don't copy `.env` or secrets into the image",
416
+ ];
417
+ if (language === "yaml" || language === "terraform")
418
+ return [
419
+ "Never hardcode secrets in config — use env vars or secrets manager",
420
+ "Pin action/provider versions to specific SHA or tag",
421
+ "Use least-privilege IAM policies",
422
+ "Enable audit logging for infrastructure changes",
423
+ ];
424
+ // Default for JS/TS
425
+ return [
426
+ "Keep dependencies updated (`npm audit`)",
427
+ "Validate all user input with schemas (zod, joi)",
428
+ "Use environment variables for secrets",
429
+ "Use `textContent` instead of `innerHTML` for user data",
430
+ ];
431
+ }
378
432
  function formatReport(findings, language, framework) {
379
433
  const ctx = framework ? ` (${framework})` : "";
380
434
  // Severity ordering
@@ -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.4",
3
+ "version": "1.9.6",
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": {