guardvibe 2.2.0 → 2.3.7

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/README.md CHANGED
@@ -5,15 +5,15 @@
5
5
  [![Node.js CI](https://github.com/goklab/guardvibe/actions/workflows/ci.yml/badge.svg)](https://github.com/goklab/guardvibe/actions/workflows/ci.yml)
6
6
  [![npm provenance](https://img.shields.io/badge/provenance-verified-brightgreen)](https://www.npmjs.com/package/guardvibe)
7
7
 
8
- **The security MCP built for vibe coding.** 277 security rules covering the entire AI-generated code journey — from first line to production deployment.
8
+ **The security MCP built for vibe coding.** 307 security rules covering the entire AI-generated code journey — from first line to production deployment.
9
9
 
10
- Works with **Claude Code, Cursor, Gemini CLI, Codex, Windsurf**, and any MCP-compatible coding agent.
10
+ Works with **Claude Code, Cursor, Gemini CLI, Codex, VS Code (Copilot), Windsurf**, and any MCP-compatible coding agent.
11
11
 
12
12
  ## Why GuardVibe
13
13
 
14
14
  Most security tools are built for enterprise security teams. GuardVibe is built for **you** — the developer using AI to build and ship web apps fast.
15
15
 
16
- - **277 security rules** purpose-built for the stacks AI agents generate
16
+ - **307 security rules** purpose-built for the stacks AI agents generate
17
17
  - **Zero setup friction** — `npx guardvibe` and you're scanning
18
18
  - **No account required** — runs 100% locally, no API keys, no cloud
19
19
  - **Understands your stack** — not generic SAST, but rules that know Next.js, Supabase, Stripe, Clerk, and the tools you actually use
@@ -39,7 +39,7 @@ GuardVibe is purpose-built for the AI coding workflow. Traditional tools are exc
39
39
  | CVE version detection | 21 packages | Extensive | Extensive |
40
40
  | Compliance mapping (SOC2, PCI-DSS, HIPAA) | Built-in | Paid tier | None |
41
41
  | SARIF CI/CD export | Yes | Yes | Limited |
42
- | Rule count | 277 (focused) | 5000+ (broad) | N/A |
42
+ | Rule count | 307 (focused) | 5000+ (broad) | N/A |
43
43
 
44
44
  **When to use GuardVibe:** You're building with AI agents and want security scanning integrated into your coding workflow — no dashboard, no account, no CI setup.
45
45
 
@@ -47,29 +47,56 @@ GuardVibe is purpose-built for the AI coding workflow. Traditional tools are exc
47
47
 
48
48
  ## Quick Start
49
49
 
50
- ### MCP setup (recommended)
50
+ ### Claude Code
51
51
 
52
52
  ```bash
53
- npx guardvibe init claude # Claude Code
54
- npx guardvibe init cursor # Cursor
55
- npx guardvibe init gemini # Gemini CLI
56
- npx guardvibe init all # All platforms
53
+ npx guardvibe init claude
57
54
  ```
58
55
 
59
- ### Pre-commit hook
56
+ Creates `.claude.json` MCP config, `.claude/settings.json` auto-scan hooks, and `CLAUDE.md` security rules. Restart Claude Code after setup.
57
+
58
+ ### Cursor
60
59
 
61
60
  ```bash
62
- npx guardvibe hook install # Blocks commits with critical/high findings
63
- npx guardvibe hook uninstall # Remove hook
61
+ npx guardvibe init cursor
64
62
  ```
65
63
 
66
- ### CI/CD (GitHub Actions)
64
+ Creates `.cursor/mcp.json` and `.cursorrules` with security rules. Restart Cursor after setup.
65
+
66
+ ### Gemini CLI
67
67
 
68
68
  ```bash
69
- npx guardvibe ci github # Generates .github/workflows/guardvibe.yml
69
+ npx guardvibe init gemini
70
70
  ```
71
71
 
72
- ### Manual MCP config
72
+ Creates `~/.gemini/settings.json` MCP config and `GEMINI.md` security rules.
73
+
74
+ ### Codex (OpenAI)
75
+
76
+ ```bash
77
+ codex mcp add guardvibe -- npx -y guardvibe
78
+ ```
79
+
80
+ ### VS Code (GitHub Copilot)
81
+
82
+ Create `.vscode/mcp.json` in your project:
83
+
84
+ ```json
85
+ {
86
+ "servers": {
87
+ "guardvibe": {
88
+ "command": "npx",
89
+ "args": ["-y", "guardvibe"]
90
+ }
91
+ }
92
+ }
93
+ ```
94
+
95
+ > **Note:** VS Code uses `"servers"`, not `"mcpServers"`.
96
+
97
+ ### Windsurf
98
+
99
+ Add to `~/.codeium/windsurf/mcp_config.json`:
73
100
 
74
101
  ```json
75
102
  {
@@ -82,6 +109,25 @@ npx guardvibe ci github # Generates .github/workflows/guardvibe.yml
82
109
  }
83
110
  ```
84
111
 
112
+ ### All platforms at once
113
+
114
+ ```bash
115
+ npx guardvibe init all # Claude + Cursor + Gemini
116
+ ```
117
+
118
+ ### Pre-commit hook
119
+
120
+ ```bash
121
+ npx guardvibe hook install # Blocks commits with critical/high findings
122
+ npx guardvibe hook uninstall # Remove hook
123
+ ```
124
+
125
+ ### CI/CD (GitHub Actions)
126
+
127
+ ```bash
128
+ npx guardvibe ci github # Generates .github/workflows/guardvibe.yml
129
+ ```
130
+
85
131
  ## What GuardVibe Scans
86
132
 
87
133
  ### Application Code
@@ -132,7 +178,7 @@ SOC2, PCI-DSS, HIPAA control mapping with compliance reports
132
178
  ### Supply Chain
133
179
  Malicious postinstall scripts, unpinned GitHub Actions, typosquat detection
134
180
 
135
- ## Tools (22 MCP tools)
181
+ ## Tools (25 MCP tools)
136
182
 
137
183
  | Tool | What it does |
138
184
  |------|-------------|
@@ -158,34 +204,38 @@ Malicious postinstall scripts, unpinned GitHub Actions, typosquat detection
158
204
  | `scan_config_change` | Compare config file versions to detect security downgrades |
159
205
  | `repo_security_posture` | Assess overall repository security posture and map sensitive areas |
160
206
  | `explain_remediation` | Get detailed remediation guidance with exploit scenarios and fix strategies |
207
+ | `scan_file` | Real-time single-file scan — designed for post-edit hooks |
208
+ | `scan_changed_files` | Scan only git-changed files — for PRs and incremental CI |
209
+ | `security_stats` | Cumulative security dashboard — scans, fixes, grade trend over time |
161
210
 
162
211
  All scanning tools support `format: "json"` for machine-readable output.
163
212
 
164
- ## Security Rules (277 rules across 23 modules)
213
+ ## Security Rules (307 rules across 23 modules)
165
214
 
166
215
  | Category | Rules | Coverage |
167
216
  |----------|-------|----------|
168
- | Core OWASP | 19 | SQL injection, XSS, CSRF, command injection, CORS, SSRF, hardcoded secrets |
169
- | Next.js App Router | 13 | Server Actions, secret exposure, auth bypass, CSP, redirects |
217
+ | Core OWASP | 38 | SQL injection, XSS, CSRF, command injection, CORS, SSRF, hardcoded secrets |
218
+ | Next.js App Router | 17 | Server Actions, secret exposure, auth bypass, CSP, redirects |
170
219
  | Auth (Clerk / Auth.js / Supabase Auth) | 16 | Middleware, secret keys, session storage, role checks, SSR cookies |
171
- | Database (Supabase / Prisma / Drizzle) | 8 | Raw queries, client exposure, service role leaks |
220
+ | Database (Supabase / Prisma / Drizzle) | 10 | Raw queries, client exposure, service role leaks |
172
221
  | OWASP API Security | 10 | BOLA/IDOR, mass assignment, pagination, rate limiting, error leaks |
173
- | Modern Stack | 30 | Zod, tRPC, Hono, GraphQL, Uploadthing, Turso, Convex, OAuth, CSP, webhooks, AI SDK |
174
- | Deployment Config | 16 | Vercel, Next.js config, Docker Compose, Fly, Render, Netlify |
222
+ | Modern Stack | 36 | Zod, tRPC, Hono, GraphQL, Uploadthing, Turso, Convex, OAuth, CSP, webhooks, AI SDK |
223
+ | Deployment Config | 20 | Vercel, Next.js config, Docker Compose, Fly, Render, Netlify, Cloudflare |
175
224
  | Payments (Stripe / Polar / Lemon) | 9 | Webhook signatures, key exposure, price manipulation |
176
225
  | Services (Resend / Upstash / Pinecone / PostHog) | 11 | API key leaks, PII tracking, email injection |
177
- | Web Security | 14 | Webhooks, CSP, .env safety, AI key exposure, Cloudflare |
226
+ | Web Security | 15 | Webhooks, CSP, .env safety, AI key exposure, cookie handling |
178
227
  | React Native / Expo | 10 | AsyncStorage secrets, deep links, ATS, hardcoded URLs |
179
228
  | Firebase | 7 | Firestore rules, admin SDK, storage, custom tokens |
180
- | AI / LLM Security | 14 | Prompt injection, MCP SSRF, excessive agency, indirect injection |
181
- | CVE Version Intelligence | 20 | Known vulnerable versions in package.json (21 CVEs) |
229
+ | AI / LLM Security | 16 | Prompt injection, MCP SSRF, excessive agency, indirect injection |
230
+ | CVE Version Intelligence | 21 | Known vulnerable versions in package.json (21 CVEs) |
182
231
  | Shell / Bash | 5 | Pipe to bash, chmod 777, rm -rf, sudo password |
183
232
  | SQL | 4 | DROP/DELETE without WHERE, stacked queries, GRANT ALL |
184
- | Supply Chain | 2 | Malicious install scripts, unpinned actions |
233
+ | Supply Chain | 10 | Malicious install scripts, unpinned actions, typosquat detection |
185
234
  | Go | 6 | SQL injection, command injection, template escaping |
186
- | Dockerfile | 5 | Root user, secrets in ENV, untagged images |
187
- | CI/CD (GitHub Actions) | 4 | Secrets interpolation, unpinned actions, write-all permissions |
188
- | Terraform | 5 | Public S3, open security groups, IAM wildcards |
235
+ | Dockerfile | 7 | Root user, secrets in ENV, untagged images, non-root user |
236
+ | CI/CD (GitHub Actions) | 7 | Secrets interpolation, unpinned actions, write-all permissions |
237
+ | Terraform | 6 | Public S3, open security groups, IAM wildcards |
238
+ | Advanced Security | 21 | ReDoS, CRLF injection, race conditions, XXE, brute force, audit logging |
189
239
  | Other Services | 5 | AWS, GCP, MongoDB, Convex, Sentry, Twilio |
190
240
 
191
241
  ## CLI Commands
@@ -304,6 +354,23 @@ app.get("/api/health", (req, res) => res.json({ ok: true }));
304
354
 
305
355
  Supports `//`, `#`, and `<!-- -->` comment styles.
306
356
 
357
+ ## GuardVibe Scans Itself
358
+
359
+ We run GuardVibe on its own codebase. In v2.3.2, GuardVibe caught a **HIGH severity ReDoS vulnerability** in its own `policy-check.ts` — a regex injection risk that the developer missed during code review.
360
+
361
+ ```
362
+ $ guardvibe scan_directory src/
363
+ Files scanned: 64
364
+ Scan duration: 102ms
365
+ Grade: B (89/100)
366
+
367
+ [HIGH] ReDoS via User-Controlled RegExp (VG107)
368
+ File: src/tools/policy-check.ts:47
369
+ Fix: escape regex metacharacters before passing to RegExp constructor
370
+ ```
371
+
372
+ The vulnerability was fixed in the same session. This is exactly the workflow GuardVibe enables: catch what humans miss, fix before it ships.
373
+
307
374
  ## How It Works
308
375
 
309
376
  ```
package/build/cli.js CHANGED
@@ -78,6 +78,14 @@ const SECURITY_RULES = `## Security Rules (enforced by GuardVibe MCP)
78
78
  - When GuardVibe reports HIGH or CRITICAL: fix immediately before moving on
79
79
  - BEFORE creating a PR: use scan_changed_files to review all changes
80
80
 
81
+ ### Scan visibility — ALWAYS report GuardVibe results to the user
82
+ - AFTER every scan: ALWAYS tell the user the GuardVibe result, even when clean
83
+ - When issues found: "GuardVibe: [count] issues found ([severity]). Fixing..."
84
+ - When clean: "GuardVibe: scanned [filename] — no issues found."
85
+ - Include the summary stats line from scan output when present
86
+ - NEVER silently consume scan results — the user must see that GuardVibe is protecting their code
87
+ - Periodically (every ~10 scans or at session start): run security_stats tool and share the dashboard with the user
88
+
81
89
  ### Writing secure code
82
90
  - Every API route handler MUST have auth check before DB access
83
91
  - Every POST endpoint MUST have input validation (zod/joi schema)
@@ -108,6 +116,8 @@ function setupSecurityGuide(platformName) {
108
116
  gemini: ["GEMINI.md"],
109
117
  };
110
118
  const entries = gitignoreEntries[platformName] || [];
119
+ // Always add .guardvibe/ (stats directory) to .gitignore
120
+ entries.push(".guardvibe/");
111
121
  if (entries.length > 0)
112
122
  addToGitignore(entries);
113
123
  }
package/build/index.js CHANGED
@@ -31,6 +31,8 @@ import { discoverPlugins } from "./plugins/loader.js";
31
31
  import { builtinRules } from "./data/rules/index.js";
32
32
  import { loadConfig } from "./utils/config.js";
33
33
  import { setRules, getRules } from "./utils/rule-registry.js";
34
+ import { recordScan, recordFix, recordSecrets, recordDependencyCVEs, recordGrade, getSummaryLine } from "./lib/stats.js";
35
+ import { securityStats } from "./tools/security-stats.js";
34
36
  const server = new McpServer({
35
37
  name: "guardvibe",
36
38
  version: pkg.version,
@@ -49,8 +51,12 @@ server.tool("check_code", "Analyze code for security vulnerabilities (OWASP Top
49
51
  }, async ({ code, language, framework, format }) => {
50
52
  const rules = getRules();
51
53
  const results = checkCode(code, language, framework, undefined, undefined, format, rules);
54
+ const findings = analyzeCode(code, language, framework, undefined, undefined, rules);
55
+ const cwd = process.cwd();
56
+ recordScan(cwd, { toolName: "check_code", filesScanned: 1, findings: findings.map(f => ({ severity: f.rule.severity, ruleId: f.rule.id })) });
57
+ const summary = getSummaryLine(cwd, findings.length, format);
52
58
  return {
53
- content: [{ type: "text", text: results }],
59
+ content: [{ type: "text", text: results + summary }],
54
60
  };
55
61
  });
56
62
  // Tool 2: Scan entire project for security vulnerabilities
@@ -65,8 +71,25 @@ server.tool("check_project", "Scan multiple files for security vulnerabilities a
65
71
  }, async ({ files, format }) => {
66
72
  const rules = getRules();
67
73
  const results = checkProject(files, format, rules);
74
+ let findingCount = 0;
75
+ const cwd = process.cwd();
76
+ try {
77
+ const parsed = JSON.parse(results);
78
+ findingCount = parsed?.summary?.total ?? 0;
79
+ const grade = parsed?.summary?.grade;
80
+ const score = parsed?.summary?.score;
81
+ if (grade && score != null)
82
+ recordGrade(cwd, grade, score);
83
+ recordScan(cwd, { toolName: "check_project", filesScanned: files.length, findings: (parsed?.findings ?? []).map((f) => ({ severity: f.severity, ruleId: f.id })) });
84
+ }
85
+ catch {
86
+ const m = /Issues found:\s*(\d+)/.exec(results);
87
+ findingCount = m ? parseInt(m[1], 10) : 0;
88
+ recordScan(cwd, { toolName: "check_project", filesScanned: files.length, findings: [] });
89
+ }
90
+ const summary = getSummaryLine(cwd, findingCount, format);
68
91
  return {
69
- content: [{ type: "text", text: results }],
92
+ content: [{ type: "text", text: results + summary }],
70
93
  };
71
94
  });
72
95
  // Tool 3: Get security documentation and best practices (renumbered from Tool 2)
@@ -117,7 +140,26 @@ server.tool("scan_directory", "Scan an entire project directory for security vul
117
140
  }, async ({ path, recursive, exclude, format, baseline }) => {
118
141
  const rules = getRules();
119
142
  const results = scanDirectory(path, recursive, exclude, format, rules, baseline);
120
- return { content: [{ type: "text", text: results }] };
143
+ // Record stats from scan_directory results
144
+ let findingCount = 0;
145
+ const { resolve: resolvePath } = await import("path");
146
+ const root = resolvePath(path);
147
+ try {
148
+ const parsed = JSON.parse(results);
149
+ findingCount = parsed?.summary?.total ?? 0;
150
+ const grade = parsed?.summary?.grade;
151
+ const score = parsed?.summary?.score;
152
+ if (grade && score != null)
153
+ recordGrade(root, grade, score);
154
+ recordScan(root, { toolName: "scan_directory", filesScanned: parsed?.metadata?.filesScanned ?? 0, findings: (parsed?.findings ?? []).map((f) => ({ severity: f.severity, ruleId: f.id })) });
155
+ }
156
+ catch {
157
+ const m = /Issues found:\s*(\d+)/.exec(results);
158
+ findingCount = m ? parseInt(m[1], 10) : 0;
159
+ recordScan(root, { toolName: "scan_directory", filesScanned: 0, findings: [] });
160
+ }
161
+ const summary = getSummaryLine(root, findingCount, format);
162
+ return { content: [{ type: "text", text: results + summary }] };
121
163
  });
122
164
  // Tool 6: Scan manifest/lockfile for dependency vulnerabilities
123
165
  server.tool("scan_dependencies", "Parse a lockfile or manifest (package.json, package-lock.json, requirements.txt, go.mod) and check all dependencies for known CVEs via the OSV database. Reads the file directly.", {
@@ -125,6 +167,19 @@ server.tool("scan_dependencies", "Parse a lockfile or manifest (package.json, pa
125
167
  format: z.enum(["markdown", "json"]).default("markdown").describe("Output format: markdown (human) or json (machine-readable for agents)"),
126
168
  }, async ({ manifest_path, format }) => {
127
169
  const results = await scanDependencies(manifest_path, format);
170
+ // Record dependency CVE stats
171
+ const { resolve: resolvePath, dirname } = await import("path");
172
+ const root = dirname(resolvePath(manifest_path));
173
+ try {
174
+ const parsed = JSON.parse(results);
175
+ const cveCount = parsed?.summary?.total ?? 0;
176
+ if (cveCount > 0)
177
+ recordDependencyCVEs(root, cveCount);
178
+ recordScan(root, { toolName: "scan_dependencies", filesScanned: 1, findings: (parsed?.packages ?? []).flatMap((p) => (p.vulnerabilities ?? []).map((v) => ({ severity: v.severity, ruleId: `DEP-${p.name}` }))) });
179
+ }
180
+ catch {
181
+ recordScan(root, { toolName: "scan_dependencies", filesScanned: 1, findings: [] });
182
+ }
128
183
  return { content: [{ type: "text", text: results }] };
129
184
  });
130
185
  // Tool 7: Scan for leaked secrets, API keys, and credentials
@@ -134,6 +189,20 @@ server.tool("scan_secrets", "Scan files and directories for leaked secrets, API
134
189
  format: z.enum(["markdown", "json"]).default("markdown").describe("Output format: markdown (human) or json (machine-readable for agents)"),
135
190
  }, async ({ path, recursive, format }) => {
136
191
  const results = scanSecrets(path, recursive, format);
192
+ // Record secret findings
193
+ let secretCount = 0;
194
+ try {
195
+ const parsed = JSON.parse(results);
196
+ secretCount = parsed?.summary?.total ?? 0;
197
+ }
198
+ catch {
199
+ const m = /Secrets found:\s*(\d+)/.exec(results);
200
+ secretCount = m ? parseInt(m[1], 10) : 0;
201
+ }
202
+ const { resolve: resolvePath } = await import("path");
203
+ if (secretCount > 0)
204
+ recordSecrets(resolvePath(path), secretCount);
205
+ recordScan(resolvePath(path), { toolName: "scan_secrets", filesScanned: 0, findings: [] });
137
206
  return { content: [{ type: "text", text: results }] };
138
207
  });
139
208
  // Tool 8: Scan git-staged files before committing
@@ -141,8 +210,21 @@ server.tool("scan_staged", "Scan git-staged files for security vulnerabilities b
141
210
  format: z.enum(["markdown", "json"]).default("markdown").describe("Output format: markdown (human) or json (machine-readable for agents)"),
142
211
  }, async ({ format }) => {
143
212
  const rules = getRules();
144
- const results = scanStaged(process.cwd(), format, rules);
145
- return { content: [{ type: "text", text: results }] };
213
+ const cwd = process.cwd();
214
+ const results = scanStaged(cwd, format, rules);
215
+ let findingCount = 0;
216
+ try {
217
+ const parsed = JSON.parse(results);
218
+ findingCount = parsed?.summary?.total ?? 0;
219
+ recordScan(cwd, { toolName: "scan_staged", filesScanned: parsed?.summary?.stagedFiles ?? 0, findings: (parsed?.findings ?? []).map((f) => ({ severity: f.severity, ruleId: f.id })) });
220
+ }
221
+ catch {
222
+ const m = /Issues found:\s*(\d+)/.exec(results);
223
+ findingCount = m ? parseInt(m[1], 10) : 0;
224
+ recordScan(cwd, { toolName: "scan_staged", filesScanned: 0, findings: [] });
225
+ }
226
+ const summary = getSummaryLine(cwd, findingCount, format);
227
+ return { content: [{ type: "text", text: results + summary }] };
146
228
  });
147
229
  // Tool 9: Generate compliance-focused security report
148
230
  server.tool("compliance_report", "Generate a compliance-focused security report mapped to SOC2, PCI-DSS, HIPAA, GDPR, or ISO27001 controls. Scans a directory and groups findings by compliance control. Includes exploit scenarios and audit evidence for each finding. Use mode=executive for a C-level summary.", {
@@ -185,6 +267,14 @@ server.tool("fix_code", "Analyze code for security vulnerabilities and return fi
185
267
  }, async ({ code, language, framework, format }) => {
186
268
  const rules = getRules();
187
269
  const results = fixCode(code, language, framework, undefined, format, rules);
270
+ // Record fix stats
271
+ try {
272
+ const parsed = JSON.parse(results);
273
+ const fixCount = parsed?.total ?? 0;
274
+ if (fixCount > 0)
275
+ recordFix(process.cwd(), fixCount);
276
+ }
277
+ catch { /* markdown format — skip */ }
188
278
  return {
189
279
  content: [{ type: "text", text: results }],
190
280
  };
@@ -312,7 +402,11 @@ server.tool("scan_file", "Scan a single file from disk for security vulnerabilit
312
402
  }
313
403
  const rules = getRules();
314
404
  const result = checkCode(content, language, undefined, resolved, dirname(resolved), format, rules);
315
- return { content: [{ type: "text", text: result }] };
405
+ const findings = analyzeCode(content, language, undefined, resolved, dirname(resolved), rules);
406
+ const cwd = dirname(resolved);
407
+ recordScan(cwd, { toolName: "scan_file", filesScanned: 1, findings: findings.map(f => ({ severity: f.rule.severity, ruleId: f.rule.id })) });
408
+ const summary = getSummaryLine(cwd, findings.length, format);
409
+ return { content: [{ type: "text", text: result + summary }] };
316
410
  });
317
411
  // Tool 24: Scan changed files only — for incremental CI/CD and PR workflows
318
412
  server.tool("scan_changed_files", "Scan only files that have changed since a given git ref (branch, commit, or HEAD~N). Ideal for PR checks, pre-push hooks, and incremental CI. Returns findings only for modified/added files.", {
@@ -366,6 +460,9 @@ server.tool("scan_changed_files", "Scan only files that have changed since a giv
366
460
  }
367
461
  catch { /* skip unreadable files */ }
368
462
  }
463
+ // Record stats
464
+ recordScan(root, { toolName: "scan_changed_files", filesScanned: changedFiles.length, findings: allFindings.map(f => ({ severity: f.severity, ruleId: f.id })) });
465
+ const statsSummary = getSummaryLine(root, allFindings.length, format);
369
466
  if (format === "json") {
370
467
  const critical = allFindings.filter(f => f.severity === "critical").length;
371
468
  const high = allFindings.filter(f => f.severity === "high").length;
@@ -373,7 +470,7 @@ server.tool("scan_changed_files", "Scan only files that have changed since a giv
373
470
  return { content: [{ type: "text", text: JSON.stringify({
374
471
  summary: { total: allFindings.length, critical, high, medium, low: 0, blocked: critical > 0 || high > 0, changedFiles: changedFiles.length },
375
472
  findings: allFindings,
376
- }) }] };
473
+ }) + statsSummary }] };
377
474
  }
378
475
  // Markdown
379
476
  const lines = [`# GuardVibe Changed Files Report`, ``, `Base: ${base}`, `Changed files: ${changedFiles.length}`, `Issues found: ${allFindings.length}`, ``];
@@ -387,7 +484,18 @@ server.tool("scan_changed_files", "Scan only files that have changed since a giv
387
484
  lines.push(`- [${f.severity.toUpperCase()}] **${f.name}** (${f.id}) in \`${f.file}\`:${f.line} — ${f.fix}`);
388
485
  }
389
486
  }
390
- return { content: [{ type: "text", text: lines.join("\n") }] };
487
+ return { content: [{ type: "text", text: lines.join("\n") + statsSummary }] };
488
+ });
489
+ // Tool 25: Security statistics dashboard
490
+ server.tool("security_stats", "Show cumulative security statistics, grade trend, and vulnerability fix progress for this project. Use this to demonstrate the value of GuardVibe security scanning over time. Data is stored locally in .guardvibe/stats.json.", {
491
+ path: z.string().default(".").describe("Project root path"),
492
+ period: z.enum(["week", "month", "all"]).default("month").describe("Time period for stats"),
493
+ format: z.enum(["markdown", "json"]).default("markdown").describe("Output format"),
494
+ }, async ({ path: projectPath, period, format }) => {
495
+ const { resolve: resolvePath } = await import("path");
496
+ const root = resolvePath(projectPath);
497
+ const results = securityStats(root, period, format);
498
+ return { content: [{ type: "text", text: results }] };
391
499
  });
392
500
  export async function startMcpServer() {
393
501
  return main();
@@ -0,0 +1,53 @@
1
+ export interface MonthlyStats {
2
+ scans: number;
3
+ filesScanned: number;
4
+ findingsTotal: number;
5
+ findingsFixed: number;
6
+ critical: number;
7
+ high: number;
8
+ medium: number;
9
+ low: number;
10
+ }
11
+ export interface GradeEntry {
12
+ date: string;
13
+ grade: string;
14
+ score: number;
15
+ }
16
+ export interface StatsData {
17
+ version: number;
18
+ firstScan: string;
19
+ lastScan: string;
20
+ totals: {
21
+ scans: number;
22
+ filesScanned: number;
23
+ findingsTotal: number;
24
+ findingsFixed: number;
25
+ critical: number;
26
+ high: number;
27
+ medium: number;
28
+ low: number;
29
+ autoFixesApplied: number;
30
+ secretsCaught: number;
31
+ dependencyCVEs: number;
32
+ };
33
+ monthly: Record<string, MonthlyStats>;
34
+ tools: Record<string, number>;
35
+ topRules: Record<string, number>;
36
+ grades: GradeEntry[];
37
+ }
38
+ export declare function loadStats(projectRoot: string): StatsData;
39
+ export interface ScanResult {
40
+ toolName: string;
41
+ filesScanned: number;
42
+ findings: Array<{
43
+ severity: string;
44
+ ruleId: string;
45
+ }>;
46
+ }
47
+ export declare function recordScan(projectRoot: string, result: ScanResult): void;
48
+ export declare function recordFix(projectRoot: string, fixCount: number): void;
49
+ export declare function recordSecrets(projectRoot: string, count: number): void;
50
+ export declare function recordDependencyCVEs(projectRoot: string, count: number): void;
51
+ export declare function recordGrade(projectRoot: string, grade: string, score: number): void;
52
+ export declare function getSummaryLine(projectRoot: string, currentFindings: number, format: "markdown" | "json"): string;
53
+ export declare function generateDashboard(projectRoot: string, period: "week" | "month" | "all", format: "markdown" | "json"): string;
@@ -0,0 +1,276 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
2
+ import { join, resolve } from "path";
3
+ // ─── Helpers ──────────────────────────────────────────────────────
4
+ function emptyStats() {
5
+ return {
6
+ version: 1,
7
+ firstScan: "",
8
+ lastScan: "",
9
+ totals: {
10
+ scans: 0,
11
+ filesScanned: 0,
12
+ findingsTotal: 0,
13
+ findingsFixed: 0,
14
+ critical: 0,
15
+ high: 0,
16
+ medium: 0,
17
+ low: 0,
18
+ autoFixesApplied: 0,
19
+ secretsCaught: 0,
20
+ dependencyCVEs: 0,
21
+ },
22
+ monthly: {},
23
+ tools: {},
24
+ topRules: {},
25
+ grades: [],
26
+ };
27
+ }
28
+ function getMonthKey() {
29
+ const d = new Date();
30
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`;
31
+ }
32
+ function getTodayKey() {
33
+ return new Date().toISOString().slice(0, 10);
34
+ }
35
+ function statsDir(projectRoot) {
36
+ return join(resolve(projectRoot), ".guardvibe");
37
+ }
38
+ function statsPath(projectRoot) {
39
+ return join(statsDir(projectRoot), "stats.json");
40
+ }
41
+ // ─── Core I/O ─────────────────────────────────────────────────────
42
+ export function loadStats(projectRoot) {
43
+ try {
44
+ const p = statsPath(projectRoot);
45
+ if (!existsSync(p))
46
+ return emptyStats();
47
+ const raw = readFileSync(p, "utf-8");
48
+ return JSON.parse(raw);
49
+ }
50
+ catch {
51
+ return emptyStats();
52
+ }
53
+ }
54
+ function saveStats(projectRoot, data) {
55
+ try {
56
+ const dir = statsDir(projectRoot);
57
+ if (!existsSync(dir))
58
+ mkdirSync(dir, { recursive: true });
59
+ writeFileSync(statsPath(projectRoot), JSON.stringify(data, null, 2), "utf-8");
60
+ }
61
+ catch {
62
+ // Stats write failure must never break scans — silently continue
63
+ }
64
+ }
65
+ function ensureMonth(data, key) {
66
+ if (!data.monthly[key]) {
67
+ data.monthly[key] = {
68
+ scans: 0, filesScanned: 0, findingsTotal: 0, findingsFixed: 0,
69
+ critical: 0, high: 0, medium: 0, low: 0,
70
+ };
71
+ }
72
+ return data.monthly[key];
73
+ }
74
+ export function recordScan(projectRoot, result) {
75
+ const data = loadStats(projectRoot);
76
+ const now = new Date().toISOString();
77
+ const month = getMonthKey();
78
+ if (!data.firstScan)
79
+ data.firstScan = now;
80
+ data.lastScan = now;
81
+ // Totals
82
+ data.totals.scans++;
83
+ data.totals.filesScanned += result.filesScanned;
84
+ data.totals.findingsTotal += result.findings.length;
85
+ // Severity counts
86
+ for (const f of result.findings) {
87
+ const sev = f.severity;
88
+ if (sev in data.totals && typeof data.totals[sev] === "number") {
89
+ data.totals[sev]++;
90
+ }
91
+ // Top rules
92
+ data.topRules[f.ruleId] = (data.topRules[f.ruleId] || 0) + 1;
93
+ }
94
+ // Tool usage
95
+ data.tools[result.toolName] = (data.tools[result.toolName] || 0) + 1;
96
+ // Monthly
97
+ const m = ensureMonth(data, month);
98
+ m.scans++;
99
+ m.filesScanned += result.filesScanned;
100
+ m.findingsTotal += result.findings.length;
101
+ for (const f of result.findings) {
102
+ const sev = f.severity;
103
+ if (sev in m && typeof m[sev] === "number") {
104
+ m[sev]++;
105
+ }
106
+ }
107
+ saveStats(projectRoot, data);
108
+ }
109
+ export function recordFix(projectRoot, fixCount) {
110
+ const data = loadStats(projectRoot);
111
+ const month = getMonthKey();
112
+ data.totals.findingsFixed += fixCount;
113
+ data.totals.autoFixesApplied += fixCount;
114
+ const m = ensureMonth(data, month);
115
+ m.findingsFixed += fixCount;
116
+ saveStats(projectRoot, data);
117
+ }
118
+ export function recordSecrets(projectRoot, count) {
119
+ const data = loadStats(projectRoot);
120
+ data.totals.secretsCaught += count;
121
+ saveStats(projectRoot, data);
122
+ }
123
+ export function recordDependencyCVEs(projectRoot, count) {
124
+ const data = loadStats(projectRoot);
125
+ data.totals.dependencyCVEs += count;
126
+ saveStats(projectRoot, data);
127
+ }
128
+ export function recordGrade(projectRoot, grade, score) {
129
+ const data = loadStats(projectRoot);
130
+ const today = getTodayKey();
131
+ // Replace today's entry if exists, otherwise append
132
+ const idx = data.grades.findIndex((g) => g.date === today);
133
+ if (idx >= 0) {
134
+ data.grades[idx] = { date: today, grade, score };
135
+ }
136
+ else {
137
+ data.grades.push({ date: today, grade, score });
138
+ // Keep last 90 days max
139
+ if (data.grades.length > 90)
140
+ data.grades = data.grades.slice(-90);
141
+ }
142
+ saveStats(projectRoot, data);
143
+ }
144
+ // ─── Summary Line (appended to scan output) ───────────────────────
145
+ export function getSummaryLine(projectRoot, currentFindings, format) {
146
+ try {
147
+ const data = loadStats(projectRoot);
148
+ const month = getMonthKey();
149
+ const m = data.monthly[month];
150
+ const monthlyFixed = m?.findingsFixed ?? 0;
151
+ const monthlyTotal = m?.findingsTotal ?? 0;
152
+ // Latest grade
153
+ const latestGrade = data.grades.length > 0
154
+ ? data.grades[data.grades.length - 1]
155
+ : null;
156
+ // Trend: compare current grade to first grade this month
157
+ const monthGrades = data.grades.filter((g) => g.date.startsWith(month));
158
+ let trend = "";
159
+ if (monthGrades.length >= 2) {
160
+ const first = monthGrades[0].score;
161
+ const last = monthGrades[monthGrades.length - 1].score;
162
+ if (last > first)
163
+ trend = " (improving)";
164
+ else if (last < first)
165
+ trend = " (declining)";
166
+ }
167
+ if (format === "json") {
168
+ return JSON.stringify({
169
+ guardvibeStats: {
170
+ sessionFindings: currentFindings,
171
+ monthlyTotal,
172
+ monthlyFixed,
173
+ allTimeFixed: data.totals.findingsFixed,
174
+ currentGrade: latestGrade?.grade ?? null,
175
+ trend: trend.replace(/[() ]/g, "") || "stable",
176
+ },
177
+ });
178
+ }
179
+ // Markdown — single line
180
+ const parts = [
181
+ `${currentFindings} issues caught`,
182
+ monthlyFixed > 0 ? `${monthlyFixed} fixed this month` : null,
183
+ latestGrade ? `Grade: ${latestGrade.grade}${trend}` : null,
184
+ ].filter(Boolean);
185
+ return `\n---\n**GuardVibe** · ${parts.join(" · ")}`;
186
+ }
187
+ catch {
188
+ return "";
189
+ }
190
+ }
191
+ // ─── Dashboard (security_stats tool) ──────────────────────────────
192
+ export function generateDashboard(projectRoot, period, format) {
193
+ const data = loadStats(projectRoot);
194
+ if (data.totals.scans === 0) {
195
+ const empty = "No security scans recorded yet. GuardVibe will track statistics automatically as you scan files.";
196
+ return format === "json"
197
+ ? JSON.stringify({ status: "empty", message: empty })
198
+ : empty;
199
+ }
200
+ const month = getMonthKey();
201
+ const m = data.monthly[month] ?? {
202
+ scans: 0, filesScanned: 0, findingsTotal: 0, findingsFixed: 0,
203
+ critical: 0, high: 0, medium: 0, low: 0,
204
+ };
205
+ // Top rules — sorted by count
206
+ const topRules = Object.entries(data.topRules)
207
+ .sort(([, a], [, b]) => b - a)
208
+ .slice(0, 5);
209
+ // Top tools — sorted by count
210
+ const topTools = Object.entries(data.tools)
211
+ .sort(([, a], [, b]) => b - a)
212
+ .slice(0, 5);
213
+ // Grade trend
214
+ const recentGrades = data.grades.slice(-7);
215
+ const gradeStr = recentGrades.map((g) => `${g.grade} (${g.date.slice(5)})`).join(" -> ");
216
+ // Fix rate
217
+ const fixRate = data.totals.findingsTotal > 0
218
+ ? Math.round((data.totals.findingsFixed / data.totals.findingsTotal) * 100)
219
+ : 0;
220
+ const monthFixRate = m.findingsTotal > 0
221
+ ? Math.round((m.findingsFixed / m.findingsTotal) * 100)
222
+ : 0;
223
+ if (format === "json") {
224
+ return JSON.stringify({
225
+ project: projectRoot,
226
+ period,
227
+ currentMonth: m,
228
+ allTime: data.totals,
229
+ fixRate: { monthly: monthFixRate, allTime: fixRate },
230
+ topRules,
231
+ topTools,
232
+ gradeHistory: recentGrades,
233
+ firstScan: data.firstScan,
234
+ lastScan: data.lastScan,
235
+ });
236
+ }
237
+ // Markdown dashboard
238
+ const lines = [
239
+ `# GuardVibe Security Dashboard`,
240
+ ``,
241
+ `**Project:** ${projectRoot}`,
242
+ `**Tracking since:** ${data.firstScan.slice(0, 10)}`,
243
+ `**Last scan:** ${data.lastScan.slice(0, 10)}`,
244
+ ``,
245
+ `## Impact Summary`,
246
+ `| Metric | This Month | All Time |`,
247
+ `|--------|-----------|----------|`,
248
+ `| Scans run | ${m.scans} | ${data.totals.scans} |`,
249
+ `| Files protected | ${m.filesScanned} | ${data.totals.filesScanned} |`,
250
+ `| Vulnerabilities caught | ${m.findingsTotal} | ${data.totals.findingsTotal} |`,
251
+ `| Vulnerabilities fixed | ${m.findingsFixed} | ${data.totals.findingsFixed} |`,
252
+ `| Fix rate | ${monthFixRate}% | ${fixRate}% |`,
253
+ `| Secrets intercepted | — | ${data.totals.secretsCaught} |`,
254
+ `| Dependency CVEs found | — | ${data.totals.dependencyCVEs} |`,
255
+ ``,
256
+ ];
257
+ if (recentGrades.length > 0) {
258
+ lines.push(`## Security Grade Trend`, gradeStr, ``);
259
+ }
260
+ if (topRules.length > 0) {
261
+ lines.push(`## Top Caught Vulnerabilities`);
262
+ for (const [ruleId, count] of topRules) {
263
+ lines.push(`- ${ruleId} — ${count} times`);
264
+ }
265
+ lines.push(``);
266
+ }
267
+ if (topTools.length > 0) {
268
+ lines.push(`## Most Used Tools`);
269
+ for (const [tool, count] of topTools) {
270
+ lines.push(`- ${tool} — ${count} calls`);
271
+ }
272
+ lines.push(``);
273
+ }
274
+ lines.push(`---`, `Protected by GuardVibe · guardvibe.dev`);
275
+ return lines.join("\n");
276
+ }
@@ -284,7 +284,7 @@ function generateRLS(stack) {
284
284
  if (stack.database.includes("prisma") || stack.database.includes("drizzle")) {
285
285
  suggestions.push({
286
286
  table: "N/A (ORM-level)",
287
- policy: `// Always filter by authenticated user\nconst items = await prisma.item.findMany({ where: { userId: session.user.id } });`,
287
+ policy: `// Always filter by authenticated user\nconst items = await prisma.item.findMany({ where: { userId: session.user.id } });`, // guardvibe-ignore VG955
288
288
  description: "Without RLS, enforce row-level access in your ORM queries. Always include user ID in WHERE clauses.",
289
289
  });
290
290
  }
@@ -347,7 +347,8 @@ export function generatePolicy(path, format = "markdown") {
347
347
  if (stack.analytics.length > 0)
348
348
  lines.push(`- Analytics: ${stack.analytics.join(", ")}`);
349
349
  lines.push(``);
350
- lines.push(`## Content-Security-Policy`, ``, "```", csp, "```", ``, `### Next.js Configuration`, ``, "```typescript", `// next.config.ts`, `async headers() {`, ` return [{`, ` source: "/(.*)",`, ` headers: [`, ` { key: "Content-Security-Policy", value: \`${csp}\` },`, ...headers.map(h => ` { key: "${h.key}", value: "${h.value}" },`), ` ]`, ` }];`, `}`, "```", ``);
350
+ lines.push(`## Content-Security-Policy`, ``, "```", csp, "```", ``, `### Next.js Configuration`, ``, "```typescript", `// next.config.ts`, `async headers() {`, // guardvibe-ignore
351
+ ` return [{`, ` source: "/(.*)",`, ` headers: [`, ` { key: "Content-Security-Policy", value: \`${csp}\` },`, ...headers.map(h => ` { key: "${h.key}", value: "${h.value}" },`), ` ]`, ` }];`, `}`, "```", ``);
351
352
  lines.push(`## CORS Policy`, ``, "```typescript", `// Recommended CORS configuration`, `const corsConfig = {`, ` allowedOrigins: ${JSON.stringify(cors.allowedOrigins)},`, ` allowedMethods: ${JSON.stringify(cors.allowedMethods)},`, ` allowedHeaders: ${JSON.stringify(cors.allowedHeaders)},`, ` maxAge: ${cors.maxAge},`, `};`, "```", ``);
352
353
  if (rls.length > 0) {
353
354
  lines.push(`## Row-Level Security Suggestions`, ``);
@@ -18,8 +18,14 @@ function isExcepted(ruleId, filePath, exceptions) {
18
18
  if (exc.files && exc.files.length > 0) {
19
19
  const matches = exc.files.some(pattern => {
20
20
  if (pattern.includes("*")) {
21
- const regex = new RegExp(pattern.replace(/\*/g, ".*"));
22
- return regex.test(filePath);
21
+ // Safe glob-to-regex: escape all regex metacharacters except our converted .*
22
+ const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
23
+ try {
24
+ return new RegExp(`^${escaped}$`).test(filePath);
25
+ }
26
+ catch {
27
+ return false; // malformed pattern — skip safely
28
+ }
23
29
  }
24
30
  return filePath.includes(pattern);
25
31
  });
@@ -0,0 +1,5 @@
1
+ /**
2
+ * security_stats MCP tool handler.
3
+ * Returns cumulative security statistics and grade trend for the project.
4
+ */
5
+ export declare function securityStats(projectRoot: string, period?: "week" | "month" | "all", format?: "markdown" | "json"): string;
@@ -0,0 +1,8 @@
1
+ import { generateDashboard } from "../lib/stats.js";
2
+ /**
3
+ * security_stats MCP tool handler.
4
+ * Returns cumulative security statistics and grade trend for the project.
5
+ */
6
+ export function securityStats(projectRoot, period = "month", format = "markdown") {
7
+ return generateDashboard(projectRoot, period, format);
8
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "guardvibe",
3
- "version": "2.2.0",
4
- "description": "Security MCP for vibe coding. 307 rules, 24 tools for Next.js, Supabase, Clerk, Stripe, Prisma, tRPC, Hono, GraphQL, Convex, Turso, Uploadthing, AI SDK, and the full AI-generated stack.",
3
+ "version": "2.3.7",
4
+ "description": "Security MCP for vibe coding. 307 rules, 25 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": {
7
7
  "guardvibe": "build/cli.js",