guardvibe 2.1.1 → 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 +96 -29
- package/build/cli.js +10 -0
- package/build/data/rules/advanced-security.d.ts +2 -0
- package/build/data/rules/advanced-security.js +274 -0
- package/build/data/rules/index.js +2 -0
- package/build/index.js +116 -8
- package/build/lib/stats.d.ts +53 -0
- package/build/lib/stats.js +276 -0
- package/build/tools/generate-policy.js +3 -2
- package/build/tools/policy-check.js +8 -2
- package/build/tools/security-stats.d.ts +5 -0
- package/build/tools/security-stats.js +8 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -5,15 +5,15 @@
|
|
|
5
5
|
[](https://github.com/goklab/guardvibe/actions/workflows/ci.yml)
|
|
6
6
|
[](https://www.npmjs.com/package/guardvibe)
|
|
7
7
|
|
|
8
|
-
**The security MCP built for vibe coding.**
|
|
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
|
-
- **
|
|
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 |
|
|
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
|
-
###
|
|
50
|
+
### Claude Code
|
|
51
51
|
|
|
52
52
|
```bash
|
|
53
|
-
npx guardvibe init claude
|
|
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
|
-
|
|
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
|
|
63
|
-
npx guardvibe hook uninstall # Remove hook
|
|
61
|
+
npx guardvibe init cursor
|
|
64
62
|
```
|
|
65
63
|
|
|
66
|
-
|
|
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
|
|
69
|
+
npx guardvibe init gemini
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
-
|
|
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 (
|
|
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 (
|
|
213
|
+
## Security Rules (307 rules across 23 modules)
|
|
165
214
|
|
|
166
215
|
| Category | Rules | Coverage |
|
|
167
216
|
|----------|-------|----------|
|
|
168
|
-
| Core OWASP |
|
|
169
|
-
| Next.js App Router |
|
|
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) |
|
|
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 |
|
|
174
|
-
| Deployment Config |
|
|
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 |
|
|
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 |
|
|
181
|
-
| CVE Version Intelligence |
|
|
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 |
|
|
233
|
+
| Supply Chain | 10 | Malicious install scripts, unpinned actions, typosquat detection |
|
|
185
234
|
| Go | 6 | SQL injection, command injection, template escaping |
|
|
186
|
-
| Dockerfile |
|
|
187
|
-
| CI/CD (GitHub Actions) |
|
|
188
|
-
| Terraform |
|
|
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
|
}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
// Advanced security rules that catch patterns AI assistants commonly generate
|
|
2
|
+
// These cover gaps in OWASP Top 10, CWE Top 25, and OWASP API Security Top 10
|
|
3
|
+
export const advancedSecurityRules = [
|
|
4
|
+
// ── HTTP Response Header Injection (CWE-113) ─────────────────────
|
|
5
|
+
{
|
|
6
|
+
id: "VG130",
|
|
7
|
+
name: "HTTP Response Header Injection",
|
|
8
|
+
severity: "high",
|
|
9
|
+
owasp: "A02:2025 Injection",
|
|
10
|
+
description: "User input is interpolated into HTTP response headers. Attackers can inject CRLF characters to add arbitrary headers (Set-Cookie, Location) or split the response.",
|
|
11
|
+
pattern: /(?:setHeader|set|append|headers\.set)\s*\(\s*["'][^"']+["']\s*,\s*(?:`[^`]*\$\{|[^"']*\+\s*(?:req\.|request\.|params\.|query\.|searchParams|input|body|user))/gi,
|
|
12
|
+
languages: ["javascript", "typescript"],
|
|
13
|
+
fix: "Never interpolate user input into response headers. Sanitize by removing \\r and \\n characters.",
|
|
14
|
+
fixCode: '// Sanitize header values\nconst safeValue = userInput.replace(/[\\r\\n]/g, "");\nres.setHeader("Content-Disposition", `attachment; filename="${safeValue}"`);',
|
|
15
|
+
compliance: ["SOC2:CC7.1", "PCI-DSS:Req6.5.1"],
|
|
16
|
+
},
|
|
17
|
+
// ── CSRF via State-Changing GET (CWE-352) ─────────────────────────
|
|
18
|
+
{
|
|
19
|
+
id: "VG131",
|
|
20
|
+
name: "State-Changing GET Request",
|
|
21
|
+
severity: "high",
|
|
22
|
+
owasp: "A01:2025 Broken Access Control",
|
|
23
|
+
description: "GET handler performs database mutations (delete, update, create). GET requests should be idempotent and safe. State-changing operations in GET handlers are vulnerable to CSRF via img tags, link prefetching, and browser preloading.",
|
|
24
|
+
pattern: /export\s+(?:async\s+)?function\s+GET\s*\([^)]*\)\s*\{[\s\S]{0,1000}?(?:\.delete\s*\(|\.update\s*\(|\.create\s*\(|\.destroy\s*\(|\.remove\s*\(|\.insert\s*\(|DELETE\s+FROM|UPDATE\s+|INSERT\s+INTO)/gi,
|
|
25
|
+
languages: ["javascript", "typescript"],
|
|
26
|
+
fix: "Move state-changing operations to POST/PUT/DELETE handlers. GET should only read data.",
|
|
27
|
+
fixCode: '// BAD: mutation in GET\nexport async function GET(req: Request) {\n await db.post.delete({ where: { id } }); // CSRF risk!\n}\n\n// GOOD: use POST/DELETE\nexport async function DELETE(req: Request) {\n await db.post.delete({ where: { id } });\n}',
|
|
28
|
+
compliance: ["SOC2:CC6.6"],
|
|
29
|
+
},
|
|
30
|
+
// ── Missing Request Body Size Limit ───────────────────────────────
|
|
31
|
+
{
|
|
32
|
+
id: "VG132",
|
|
33
|
+
name: "Missing Request Body Size Limit",
|
|
34
|
+
severity: "medium",
|
|
35
|
+
owasp: "API4:2023 Unrestricted Resource Consumption",
|
|
36
|
+
description: "API endpoint reads request body without size limit. Attackers can send multi-gigabyte payloads to exhaust server memory and cause denial of service.",
|
|
37
|
+
pattern: /export\s+(?:async\s+)?function\s+(?:POST|PUT|PATCH)\s*\([^)]*\)\s*\{(?:(?!content-length|maxBodySize|limit|MAX_)[\s\S]){5,}?(?:req\.json|req\.text|req\.body|req\.formData|request\.json|request\.text)\s*\(\s*\)/g,
|
|
38
|
+
languages: ["javascript", "typescript"],
|
|
39
|
+
fix: "Check Content-Length header before parsing body, or use a body parser with size limit.",
|
|
40
|
+
fixCode: '// Check body size before parsing\nexport async function POST(req: Request) {\n const contentLength = parseInt(req.headers.get("content-length") || "0");\n if (contentLength > 1024 * 1024) { // 1MB limit\n return new Response("Payload too large", { status: 413 });\n }\n const body = await req.json();\n}',
|
|
41
|
+
compliance: ["SOC2:CC7.1"],
|
|
42
|
+
},
|
|
43
|
+
// ── Race Condition in Check-Then-Act ──────────────────────────────
|
|
44
|
+
{
|
|
45
|
+
id: "VG133",
|
|
46
|
+
name: "Race Condition: Check-Then-Act Without Transaction",
|
|
47
|
+
severity: "high",
|
|
48
|
+
owasp: "A04:2025 Insecure Design",
|
|
49
|
+
description: "Code reads a value, checks a condition, then updates based on the check — without a database transaction. Two concurrent requests can both pass the check before either writes, leading to double-spending, overselling, or duplicate operations.",
|
|
50
|
+
pattern: /(?:findUnique|findFirst|findOne|findById)\s*\([\s\S]{0,200}?\)\s*;?\s*\n[\s\S]{0,300}?if\s*\([\s\S]{0,200}?\)\s*\{[\s\S]{0,500}?(?:\.update\s*\(|\.delete\s*\(|\.decrement|\.increment)(?:(?!\$transaction|\.transaction|BEGIN|SERIALIZABLE|FOR UPDATE|NOWAIT)[\s\S]){0,300}?\}/g,
|
|
51
|
+
languages: ["javascript", "typescript"],
|
|
52
|
+
fix: "Wrap check-then-act sequences in a database transaction, or use atomic operations (e.g., UPDATE WHERE balance >= amount).",
|
|
53
|
+
fixCode: '// BAD: race condition\nconst account = await db.account.findUnique({ where: { id } });\nif (account.balance >= 100) {\n await db.account.update({ where: { id }, data: { balance: { decrement: 100 } } });\n}\n\n// GOOD: atomic transaction\nawait db.$transaction(async (tx) => {\n const account = await tx.account.findUnique({ where: { id } });\n if (account.balance < 100) throw new Error("Insufficient");\n await tx.account.update({ where: { id }, data: { balance: { decrement: 100 } } });\n});',
|
|
54
|
+
compliance: ["SOC2:CC7.1"],
|
|
55
|
+
},
|
|
56
|
+
// ── WebSocket Without Authentication ──────────────────────────────
|
|
57
|
+
{
|
|
58
|
+
id: "VG134",
|
|
59
|
+
name: "WebSocket Connection Without Authentication",
|
|
60
|
+
severity: "high",
|
|
61
|
+
owasp: "A01:2025 Broken Access Control",
|
|
62
|
+
description: "WebSocket server accepts connections without verifying authentication. Any client can connect and receive or send data.",
|
|
63
|
+
pattern: /(?:WebSocketServer|WebSocket\.Server|new\s+Server)\s*\([\s\S]{0,200}?\)[\s\S]{0,300}?\.on\s*\(\s*["']connection["'][\s\S]{0,500}?(?:(?!auth|token|verify|session|cookie|jwt|bearer)[\s\S]){10,}?\.on\s*\(\s*["']message["']/gi,
|
|
64
|
+
languages: ["javascript", "typescript"],
|
|
65
|
+
fix: "Verify authentication token in the WebSocket upgrade request or first message.",
|
|
66
|
+
fixCode: '// Verify auth on connection\nwss.on("connection", (ws, req) => {\n const token = new URL(req.url!, "http://localhost").searchParams.get("token");\n if (!verifyToken(token)) { ws.close(1008, "Unauthorized"); return; }\n ws.on("message", (msg) => { /* handle */ });\n});',
|
|
67
|
+
compliance: ["SOC2:CC6.6"],
|
|
68
|
+
},
|
|
69
|
+
// ── SSE/Streaming Without Authentication ──────────────────────────
|
|
70
|
+
{
|
|
71
|
+
id: "VG135",
|
|
72
|
+
name: "Server-Sent Events Without Authentication",
|
|
73
|
+
severity: "high",
|
|
74
|
+
owasp: "A01:2025 Broken Access Control",
|
|
75
|
+
description: "Server-Sent Events endpoint streams data without authentication check. Anyone can subscribe and receive real-time updates.",
|
|
76
|
+
pattern: /["']text\/event-stream["'][\s\S]{0,500}?(?:export\s+(?:async\s+)?function\s+GET|new\s+Response\s*\()(?:(?!auth\s*\(|getServerSession|currentUser|verifyToken|session|protect)[\s\S]){5,}/gi,
|
|
77
|
+
languages: ["javascript", "typescript"],
|
|
78
|
+
fix: "Add authentication check before establishing SSE connection.",
|
|
79
|
+
compliance: ["SOC2:CC6.6"],
|
|
80
|
+
},
|
|
81
|
+
// ── postMessage Without Origin Check ──────────────────────────────
|
|
82
|
+
{
|
|
83
|
+
id: "VG136",
|
|
84
|
+
name: "postMessage Handler Without Origin Validation",
|
|
85
|
+
severity: "high",
|
|
86
|
+
owasp: "A07:2025 Cross-Site Scripting",
|
|
87
|
+
description: "Window message event handler processes data without checking event.origin. Any page (including malicious iframes) can send messages to this handler.",
|
|
88
|
+
pattern: /addEventListener\s*\(\s*["']message["']\s*,\s*(?:async\s+)?(?:\([^)]*\)|(?:event|e|evt|msg))\s*(?:=>|{)(?:(?!\.origin|event\.source)[\s\S]){5,}?(?:JSON\.parse|\.data|innerHTML|setState|dispatch|update|execute)/gi,
|
|
89
|
+
languages: ["javascript", "typescript"],
|
|
90
|
+
fix: "Always check event.origin against trusted origins before processing message data.",
|
|
91
|
+
fixCode: '// Always validate origin\nwindow.addEventListener("message", (event) => {\n if (event.origin !== "https://trusted.example.com") return;\n const data = JSON.parse(event.data);\n processData(data);\n});',
|
|
92
|
+
compliance: ["SOC2:CC7.1"],
|
|
93
|
+
},
|
|
94
|
+
// ── Debug/Internal Endpoint Exposed ───────────────────────────────
|
|
95
|
+
{
|
|
96
|
+
id: "VG137",
|
|
97
|
+
name: "Debug Endpoint Exposes System Information",
|
|
98
|
+
severity: "critical",
|
|
99
|
+
owasp: "A05:2025 Security Misconfiguration",
|
|
100
|
+
description: "Route exposes system internals (process.env, process.memoryUsage, os.cpus, debug data) that help attackers map the infrastructure.",
|
|
101
|
+
pattern: /(?:\/debug|\/internal|\/_internal|\/test|\/dev)\b[\s\S]{0,300}?(?:process\.env|process\.memoryUsage|os\.cpus|os\.hostname|process\.uptime|process\.version)/gi,
|
|
102
|
+
languages: ["javascript", "typescript"],
|
|
103
|
+
fix: "Remove debug endpoints from production code, or protect them with authentication and restrict to internal networks.",
|
|
104
|
+
compliance: ["SOC2:CC6.1"],
|
|
105
|
+
},
|
|
106
|
+
// ── Plaintext Password Comparison ─────────────────────────────────
|
|
107
|
+
{
|
|
108
|
+
id: "VG138",
|
|
109
|
+
name: "Plaintext Password Comparison",
|
|
110
|
+
severity: "critical",
|
|
111
|
+
owasp: "A02:2025 Cryptographic Failures",
|
|
112
|
+
description: "Password is compared using direct string equality (=== or ==) instead of a hashing function. This means passwords are stored or transmitted in plaintext.",
|
|
113
|
+
pattern: /(?:password|passwd|pwd)\s*(?:===|!==|==|!=)\s*(?:(?:req|request|body|input|data|form|user)[\.\[]|["'])/gi,
|
|
114
|
+
languages: ["javascript", "typescript", "python"],
|
|
115
|
+
fix: "Never compare passwords directly. Use bcrypt.compare() or argon2.verify() to compare against hashed passwords.",
|
|
116
|
+
fixCode: '// BAD: plaintext comparison\nif (user.password === inputPassword) { ... }\n\n// GOOD: hash comparison\nimport bcrypt from "bcrypt";\nconst valid = await bcrypt.compare(inputPassword, user.passwordHash);\nif (!valid) return new Response("Invalid", { status: 401 });',
|
|
117
|
+
compliance: ["SOC2:CC6.1", "PCI-DSS:Req8"],
|
|
118
|
+
},
|
|
119
|
+
// ── TLS Certificate Verification Disabled ─────────────────────────
|
|
120
|
+
{
|
|
121
|
+
id: "VG139",
|
|
122
|
+
name: "TLS Certificate Verification Disabled",
|
|
123
|
+
severity: "critical",
|
|
124
|
+
owasp: "A02:2025 Cryptographic Failures",
|
|
125
|
+
description: "TLS certificate verification is disabled, allowing man-in-the-middle attacks. All HTTPS connections become insecure.",
|
|
126
|
+
pattern: /(?:NODE_TLS_REJECT_UNAUTHORIZED\s*=\s*["']0["']|rejectUnauthorized\s*:\s*false|verify\s*=\s*False|InsecureSkipVerify\s*:\s*true)/gi,
|
|
127
|
+
languages: ["javascript", "typescript", "python", "go"],
|
|
128
|
+
fix: "Never disable TLS verification in production. Fix certificate issues instead.",
|
|
129
|
+
fixCode: '// BAD: disables all TLS verification\n// process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";\n// const agent = new https.Agent({ rejectUnauthorized: false });\n\n// GOOD: fix the certificate issue\n// - Use valid certificates (Let\'s Encrypt)\n// - Add CA certificate to Node: --use-openssl-ca\n// - For self-signed dev certs: only disable in NODE_ENV=development',
|
|
130
|
+
compliance: ["SOC2:CC6.1", "PCI-DSS:Req4.1"],
|
|
131
|
+
},
|
|
132
|
+
// ── XXE (XML External Entity) ─────────────────────────────────────
|
|
133
|
+
{
|
|
134
|
+
id: "VG140",
|
|
135
|
+
name: "XML Parsing Without Disabling External Entities",
|
|
136
|
+
severity: "high",
|
|
137
|
+
owasp: "A02:2025 Injection",
|
|
138
|
+
description: "XML is parsed without disabling external entity resolution. Attackers can use XXE to read local files, perform SSRF, or cause denial of service.",
|
|
139
|
+
pattern: /(?:parseStringPromise|parseString|DOMParser|xml2js|xmldom|libxmljs|(?:fast-xml-parser|XMLParser))\s*[\.(](?:(?!processExternalEntities\s*:\s*false|noent\s*:\s*false|resolveExternals\s*:\s*false|entityMode|FORBID_DTD)[\s\S]){5,}?(?:req\.|request\.|body|input|data|text)/gi,
|
|
140
|
+
languages: ["javascript", "typescript"],
|
|
141
|
+
fix: "Disable external entity processing in your XML parser configuration.",
|
|
142
|
+
fixCode: '// xml2js: external entities disabled by default (safe)\nimport { parseStringPromise } from "xml2js";\nconst result = await parseStringPromise(xmlInput);\n\n// fast-xml-parser: safe by default\nimport { XMLParser } from "fast-xml-parser";\nconst parser = new XMLParser({ processEntities: false });\nconst result = parser.parse(xmlInput);',
|
|
143
|
+
compliance: ["SOC2:CC7.1", "PCI-DSS:Req6.5.1"],
|
|
144
|
+
},
|
|
145
|
+
// ── YAML Unsafe Load ──────────────────────────────────────────────
|
|
146
|
+
{
|
|
147
|
+
id: "VG141",
|
|
148
|
+
name: "YAML Parsed with Unsafe Loader",
|
|
149
|
+
severity: "high",
|
|
150
|
+
owasp: "A08:2025 Software and Data Integrity Failures",
|
|
151
|
+
description: "YAML is parsed using an unsafe loader that can execute arbitrary code via !!js/function, !!python/object, or other language-specific tags.",
|
|
152
|
+
pattern: /yaml\.(?:load|unsafeLoad)\s*\(\s*(?![\s\S]{0,30}?(?:JSON_SCHEMA|FAILSAFE_SCHEMA|CORE_SCHEMA|safeLoad))/gi,
|
|
153
|
+
languages: ["javascript", "typescript", "python"],
|
|
154
|
+
fix: "Use yaml.safeLoad() or yaml.load(input, { schema: yaml.JSON_SCHEMA }).",
|
|
155
|
+
fixCode: '// BAD: allows code execution\n// yaml.load(userInput)\n\n// GOOD: safe schema\nimport yaml from "js-yaml";\nconst config = yaml.load(userInput, { schema: yaml.JSON_SCHEMA });',
|
|
156
|
+
compliance: ["SOC2:CC7.1"],
|
|
157
|
+
},
|
|
158
|
+
// ── Missing Subresource Integrity ─────────────────────────────────
|
|
159
|
+
{
|
|
160
|
+
id: "VG142",
|
|
161
|
+
name: "External Script Without Subresource Integrity",
|
|
162
|
+
severity: "medium",
|
|
163
|
+
owasp: "A08:2025 Software and Data Integrity Failures",
|
|
164
|
+
description: "External script loaded from CDN without integrity attribute. If the CDN is compromised, malicious code executes in your users' browsers.",
|
|
165
|
+
pattern: /<script\s+[^>]*src\s*=\s*["']https?:\/\/(?:(?!integrity)[\s\S])*?>/gi,
|
|
166
|
+
languages: ["html", "javascript", "typescript"],
|
|
167
|
+
fix: "Add integrity and crossorigin attributes to external script tags.",
|
|
168
|
+
fixCode: '<!-- Add SRI hash -->\n<script\n src="https://cdn.example.com/lib.js"\n integrity="sha384-HASH_HERE"\n crossorigin="anonymous"\n></script>\n\n<!-- Generate hash: openssl dgst -sha384 -binary lib.js | base64 -->',
|
|
169
|
+
compliance: ["SOC2:CC7.1"],
|
|
170
|
+
},
|
|
171
|
+
// ── CSP Missing frame-ancestors ───────────────────────────────────
|
|
172
|
+
{
|
|
173
|
+
id: "VG143",
|
|
174
|
+
name: "CSP Missing frame-ancestors Directive",
|
|
175
|
+
severity: "medium",
|
|
176
|
+
owasp: "A05:2025 Security Misconfiguration",
|
|
177
|
+
description: "Content-Security-Policy is set but lacks frame-ancestors directive. Without it, the page can be embedded in malicious iframes for clickjacking attacks. frame-ancestors supersedes X-Frame-Options.",
|
|
178
|
+
pattern: /Content-Security-Policy["']\s*[,:]\s*["'][^"']*(?:default-src|script-src)(?:(?!frame-ancestors)[^"'])*["']/gi,
|
|
179
|
+
languages: ["javascript", "typescript"],
|
|
180
|
+
fix: "Add frame-ancestors 'self' to your Content-Security-Policy header.",
|
|
181
|
+
fixCode: '// Add frame-ancestors to CSP\nheaders: [\n {\n key: "Content-Security-Policy",\n value: "default-src \'self\'; frame-ancestors \'self\'; script-src \'self\'"\n }\n]',
|
|
182
|
+
compliance: ["SOC2:CC6.1"],
|
|
183
|
+
},
|
|
184
|
+
// ── Missing Referrer-Policy ───────────────────────────────────────
|
|
185
|
+
{
|
|
186
|
+
id: "VG144",
|
|
187
|
+
name: "Missing Referrer-Policy Header",
|
|
188
|
+
severity: "medium",
|
|
189
|
+
owasp: "A05:2025 Security Misconfiguration",
|
|
190
|
+
description: "Security headers are configured but Referrer-Policy is missing. Without it, the full URL (including query parameters with tokens/IDs) is sent to external sites in the Referer header.",
|
|
191
|
+
pattern: /(?:async\s+)?headers\s*\(\s*\)\s*\{[\s\S]{10,}?(?:X-Frame-Options|Strict-Transport-Security|Content-Security-Policy)(?:(?!Referrer-Policy)[\s\S]){10,}?\}/g,
|
|
192
|
+
languages: ["javascript", "typescript"],
|
|
193
|
+
fix: "Add Referrer-Policy: strict-origin-when-cross-origin to your security headers.",
|
|
194
|
+
fixCode: '{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" }',
|
|
195
|
+
compliance: ["SOC2:CC6.1"],
|
|
196
|
+
},
|
|
197
|
+
// ── Missing Permissions-Policy ────────────────────────────────────
|
|
198
|
+
{
|
|
199
|
+
id: "VG145",
|
|
200
|
+
name: "Missing Permissions-Policy Header",
|
|
201
|
+
severity: "medium",
|
|
202
|
+
owasp: "A05:2025 Security Misconfiguration",
|
|
203
|
+
description: "Security headers are configured but Permissions-Policy is missing. Without it, embedded iframes and scripts can access camera, microphone, geolocation, and other sensitive browser APIs.",
|
|
204
|
+
pattern: /(?:async\s+)?headers\s*\(\s*\)\s*\{[\s\S]{10,}?(?:X-Frame-Options|Strict-Transport-Security|Content-Security-Policy)(?:(?!Permissions-Policy)[\s\S]){10,}?\}/g,
|
|
205
|
+
languages: ["javascript", "typescript"],
|
|
206
|
+
fix: "Add Permissions-Policy header to restrict browser API access.",
|
|
207
|
+
fixCode: '{ key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=()" }',
|
|
208
|
+
compliance: ["SOC2:CC6.1"],
|
|
209
|
+
},
|
|
210
|
+
// ── Unquoted .env Value ───────────────────────────────────────────
|
|
211
|
+
{
|
|
212
|
+
id: "VG146",
|
|
213
|
+
name: "Unquoted .env Value with Special Characters",
|
|
214
|
+
severity: "medium",
|
|
215
|
+
owasp: "A05:2025 Security Misconfiguration",
|
|
216
|
+
description: "Environment variable value contains special characters but is not quoted. This can cause parsing errors or shell injection when the .env file is sourced.",
|
|
217
|
+
pattern: /^[A-Z_]+=(?:[^"'\s]*[@#$&|;`\\(){}[\]!<>^~*?])/gm,
|
|
218
|
+
languages: ["shell"],
|
|
219
|
+
fix: "Quote .env values that contain special characters.",
|
|
220
|
+
fixCode: '# BAD: unquoted special characters\nDATABASE_URL=postgres://user:p@ss@host/db\n\n# GOOD: quoted\nDATABASE_URL="postgres://user:p@ss@host/db"',
|
|
221
|
+
compliance: ["SOC2:CC6.1"],
|
|
222
|
+
},
|
|
223
|
+
// ── Audit Logging Missing for Critical Operations ─────────────────
|
|
224
|
+
{
|
|
225
|
+
id: "VG147",
|
|
226
|
+
name: "Critical Operation Without Audit Logging",
|
|
227
|
+
severity: "medium",
|
|
228
|
+
owasp: "A09:2025 Security Logging and Monitoring Failures",
|
|
229
|
+
description: "Destructive database operation (delete user, change role, update payment) has no audit logging. Without audit trails, security incidents cannot be investigated.",
|
|
230
|
+
pattern: /(?:deleteUser|deleteAccount|updateRole|changeRole|cancelSubscription|refund|transferFunds|resetPassword|changePassword|deleteOrg|removeUser|banUser|suspendUser|updatePermission)\s*(?:=\s*async|\([\s\S]*?\)\s*(?:=>|{))[\s\S]{0,500}?(?:\.delete\s*\(|\.update\s*\(|\.destroy\s*\()(?:(?!console\.log|logger\.|audit\.|log\.|createAuditLog|logAction|trackEvent|analytics)[\s\S]){0,300}?(?:return|Response)/gi,
|
|
231
|
+
languages: ["javascript", "typescript"],
|
|
232
|
+
fix: "Add audit logging for all critical operations: who did what, when, and to whom.",
|
|
233
|
+
fixCode: 'async function deleteUser(targetId: string) {\n const { userId } = await auth();\n await db.user.delete({ where: { id: targetId } });\n // Audit log\n await db.auditLog.create({\n data: { action: "DELETE_USER", actorId: userId, targetId, timestamp: new Date() }\n });\n}',
|
|
234
|
+
compliance: ["SOC2:CC7.2", "HIPAA:§164.312(b)", "GDPR:Art30"],
|
|
235
|
+
},
|
|
236
|
+
// ── Login Without Brute Force Protection ──────────────────────────
|
|
237
|
+
{
|
|
238
|
+
id: "VG148",
|
|
239
|
+
name: "Login Endpoint Without Brute Force Protection",
|
|
240
|
+
severity: "high",
|
|
241
|
+
owasp: "A07:2025 Identification and Authentication Failures",
|
|
242
|
+
description: "Login/authentication endpoint compares passwords without rate limiting or account lockout. Attackers can try unlimited password combinations.",
|
|
243
|
+
pattern: /(?:signIn|login|authenticate|logIn)\b[\s\S]{0,500}?(?:bcrypt\.compare|argon2\.verify|compare|verify)[\s\S]{0,300}?(?:(?!rateLimit|limiter|throttle|lockout|maxAttempts|failedAttempts|loginAttempts|Ratelimit)[\s\S]){5,}?(?:return|Response|res\.)/gi,
|
|
244
|
+
languages: ["javascript", "typescript"],
|
|
245
|
+
fix: "Add rate limiting and account lockout to login endpoints.",
|
|
246
|
+
fixCode: '// Add rate limiting to login\nimport { Ratelimit } from "@upstash/ratelimit";\nconst loginLimiter = new Ratelimit({\n redis: Redis.fromEnv(),\n limiter: Ratelimit.slidingWindow(5, "15 m"), // 5 attempts per 15 min\n});\n\nconst { success } = await loginLimiter.limit(email);\nif (!success) return new Response("Too many attempts", { status: 429 });',
|
|
247
|
+
compliance: ["SOC2:CC6.6", "PCI-DSS:Req8"],
|
|
248
|
+
},
|
|
249
|
+
// ── Multi-Tenant Data Leak ────────────────────────────────────────
|
|
250
|
+
{
|
|
251
|
+
id: "VG149",
|
|
252
|
+
name: "Multi-Tenant Query Without Tenant Scoping",
|
|
253
|
+
severity: "high",
|
|
254
|
+
owasp: "A01:2025 Broken Access Control",
|
|
255
|
+
description: "Endpoint authenticates a user/org but queries the database without filtering by tenant ID. Returns data from ALL tenants instead of only the authenticated tenant's data.",
|
|
256
|
+
pattern: /(?:orgId|tenantId|organizationId|org_id|tenant_id)\s*[\s\S]{0,200}?(?:findMany|findAll|getAll|fetchAll)\s*\(\s*(?:\{(?:(?!orgId|tenantId|organizationId|org_id|tenant_id)[\s\S]){5,}?\}|\s*\))/gi,
|
|
257
|
+
languages: ["javascript", "typescript"],
|
|
258
|
+
fix: "Always include the tenant/org ID in the WHERE clause of every query.",
|
|
259
|
+
fixCode: '// BAD: returns all tenants\' data\nconst { orgId } = await auth();\nconst items = await db.item.findMany(); // missing orgId filter!\n\n// GOOD: scoped to tenant\nconst items = await db.item.findMany({ where: { orgId } });',
|
|
260
|
+
compliance: ["SOC2:CC6.1", "HIPAA:§164.312(a)"],
|
|
261
|
+
},
|
|
262
|
+
// ── Lockfile in .gitignore ────────────────────────────────────────
|
|
263
|
+
{
|
|
264
|
+
id: "VG150",
|
|
265
|
+
name: "Package Lockfile Excluded from Git",
|
|
266
|
+
severity: "high",
|
|
267
|
+
owasp: "A08:2025 Software and Data Integrity Failures",
|
|
268
|
+
description: "package-lock.json, yarn.lock, or pnpm-lock.yaml is in .gitignore. Without a committed lockfile, builds are non-reproducible and vulnerable to dependency confusion and supply chain attacks.",
|
|
269
|
+
pattern: /^(?:package-lock\.json|yarn\.lock|pnpm-lock\.yaml)\s*$/gm,
|
|
270
|
+
languages: ["shell"],
|
|
271
|
+
fix: "Remove the lockfile from .gitignore and commit it to the repository.",
|
|
272
|
+
compliance: ["SOC2:CC7.1"],
|
|
273
|
+
},
|
|
274
|
+
];
|
|
@@ -20,6 +20,7 @@ import { supplyChainRules } from "./supply-chain.js";
|
|
|
20
20
|
import { cveVersionRules } from "./cve-versions.js";
|
|
21
21
|
import { apiSecurityRules } from "./api-security.js";
|
|
22
22
|
import { modernStackRules } from "./modern-stack.js";
|
|
23
|
+
import { advancedSecurityRules } from "./advanced-security.js";
|
|
23
24
|
import { enrichRulesWithCompliance } from "../compliance-metadata.js";
|
|
24
25
|
export const owaspRules = enrichRulesWithCompliance([
|
|
25
26
|
...coreRules,
|
|
@@ -44,6 +45,7 @@ export const owaspRules = enrichRulesWithCompliance([
|
|
|
44
45
|
...cveVersionRules,
|
|
45
46
|
...apiSecurityRules,
|
|
46
47
|
...modernStackRules,
|
|
48
|
+
...advancedSecurityRules,
|
|
47
49
|
]);
|
|
48
50
|
// Alias for clarity — these are the built-in rules without plugins
|
|
49
51
|
export const builtinRules = owaspRules;
|
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
|
-
|
|
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
|
|
145
|
-
|
|
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
|
-
|
|
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() {`,
|
|
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
|
-
|
|
22
|
-
|
|
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,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.
|
|
4
|
-
"description": "Security MCP for vibe coding.
|
|
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",
|