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
|
|
292
|
-
if (
|
|
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
|
|
329
|
-
if (
|
|
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
|
|
409
|
-
if (
|
|
410
|
-
|
|
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
|
|
447
|
-
if (
|
|
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
|
-
|
|
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
|
+
"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": {
|