guardvibe 1.7.0 → 1.8.0

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
@@ -1,6 +1,6 @@
1
1
  # GuardVibe
2
2
 
3
- **The security MCP built for vibe coding.** 267 security rules covering the entire AI-generated code journey — from first line to production deployment.
3
+ **The security MCP built for vibe coding.** 277 security rules covering the entire AI-generated code journey — from first line to production deployment.
4
4
 
5
5
  Works with **Claude Code, Cursor, Gemini CLI, Codex, Windsurf**, and any MCP-compatible coding agent.
6
6
 
@@ -8,7 +8,7 @@ Works with **Claude Code, Cursor, Gemini CLI, Codex, Windsurf**, and any MCP-com
8
8
 
9
9
  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.
10
10
 
11
- - **267 security rules** purpose-built for the stacks AI agents generate
11
+ - **277 security rules** purpose-built for the stacks AI agents generate
12
12
  - **Zero setup friction** — `npx guardvibe` and you're scanning
13
13
  - **No account required** — runs 100% locally, no API keys, no cloud
14
14
  - **Understands your stack** — not generic SAST, but rules that know Next.js, Supabase, Stripe, Clerk, and the tools you actually use
@@ -34,7 +34,7 @@ GuardVibe is purpose-built for the AI coding workflow. Traditional tools are exc
34
34
  | CVE version detection | 21 packages | Extensive | Extensive |
35
35
  | Compliance mapping (SOC2, PCI-DSS, HIPAA) | Built-in | Paid tier | None |
36
36
  | SARIF CI/CD export | Yes | Yes | Limited |
37
- | Rule count | 267 (focused) | 5000+ (broad) | N/A |
37
+ | Rule count | 277 (focused) | 5000+ (broad) | N/A |
38
38
 
39
39
  **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.
40
40
 
@@ -127,7 +127,7 @@ SOC2, PCI-DSS, HIPAA control mapping with compliance reports
127
127
  ### Supply Chain
128
128
  Malicious postinstall scripts, unpinned GitHub Actions, typosquat detection
129
129
 
130
- ## Tools (12 MCP tools)
130
+ ## Tools (22 MCP tools)
131
131
 
132
132
  | Tool | What it does |
133
133
  |------|-------------|
@@ -143,10 +143,20 @@ Malicious postinstall scripts, unpinned GitHub Actions, typosquat detection
143
143
  | `export_sarif` | SARIF v2.1.0 export for CI/CD integration |
144
144
  | `get_security_docs` | Security best practices and guides |
145
145
  | `fix_code` | **Auto-fix suggestions** with concrete patches for AI agents |
146
+ | `audit_config` | Audit project configuration files for cross-file security misconfigurations |
147
+ | `generate_policy` | Detect project stack and generate tailored security policies (CSP, CORS, RLS) |
148
+ | `review_pr` | Review PR diff for security issues with severity gating |
149
+ | `scan_secrets_history` | Scan git history for leaked secrets (active and removed) |
150
+ | `policy_check` | Check project against compliance policies defined in .guardviberc |
151
+ | `analyze_dataflow` | Track tainted data flows from user input to dangerous sinks |
152
+ | `check_command` | Analyze shell commands for security risks before execution |
153
+ | `scan_config_change` | Compare config file versions to detect security downgrades |
154
+ | `repo_security_posture` | Assess overall repository security posture and map sensitive areas |
155
+ | `explain_remediation` | Get detailed remediation guidance with exploit scenarios and fix strategies |
146
156
 
147
157
  All scanning tools support `format: "json"` for machine-readable output.
148
158
 
149
- ## Security Rules (267 rules across 23 modules)
159
+ ## Security Rules (277 rules across 23 modules)
150
160
 
151
161
  | Category | Rules | Coverage |
152
162
  |----------|-------|----------|
@@ -311,6 +321,104 @@ Tested on a real 644-file Next.js + Supabase project:
311
321
  - False positive rate: **near zero** (comment/string filtering, human-readable text detection)
312
322
  - Detection rate: **100%** on known vulnerability patterns
313
323
 
324
+ ## Troubleshooting
325
+
326
+ ### MCP connection issues
327
+
328
+ If your AI agent cannot connect to GuardVibe:
329
+
330
+ 1. **Restart your IDE/agent.** MCP servers are started by the host application. After running `npx guardvibe init`, restart Claude Code, Cursor, or Gemini CLI for the config to take effect.
331
+ 2. **Check the config path.** Run `npx guardvibe init claude` again and verify the output shows the correct config file location (`.claude.json` in your project root for Claude Code, `.cursor/mcp.json` for Cursor).
332
+ 3. **Verify Node.js version.** GuardVibe requires Node.js >= 18.0.0. Check with `node --version`.
333
+ 4. **Check npx cache.** If you upgraded GuardVibe and the old version is cached, run `npx -y guardvibe@latest` to force the latest version.
334
+
335
+ ### Node.js version requirements
336
+
337
+ GuardVibe requires **Node.js >= 18.0.0**. Earlier versions will fail with syntax errors or missing APIs. Node.js 22 LTS is recommended.
338
+
339
+ ### False positives
340
+
341
+ If a rule triggers on safe code:
342
+
343
+ - **Inline suppression:** Add `// guardvibe-ignore VG001` on the same line, or `// guardvibe-ignore-next-line VG001` on the line above. Supports `//`, `#`, and `<!-- -->` comment styles.
344
+ - **Config exclusion:** Add the rule ID to `rules.disable` in `.guardviberc`:
345
+ ```json
346
+ { "rules": { "disable": ["VG030"] } }
347
+ ```
348
+ - **Path exclusion:** Add directories to `scan.exclude` in `.guardviberc`:
349
+ ```json
350
+ { "scan": { "exclude": ["fixtures/", "test-data/"] } }
351
+ ```
352
+
353
+ ### Pre-commit hook issues
354
+
355
+ - **Hook not running:** Verify the hook file exists at `.git/hooks/pre-commit` and is executable (`chmod +x .git/hooks/pre-commit`).
356
+ - **Hook blocking valid commits:** Use `git commit --no-verify` to skip the hook temporarily, then investigate the findings.
357
+ - **Removing the hook:** Run `npx guardvibe hook uninstall`.
358
+
359
+ ## Security Model
360
+
361
+ GuardVibe is designed for use on sensitive and proprietary codebases:
362
+
363
+ - **100% local execution.** All scanning happens on your machine. No code, findings, or metadata are sent to any server.
364
+ - **No accounts, no API keys, no telemetry.** There is no signup, no cloud dashboard, and no usage tracking of any kind.
365
+ - **One optional network call.** The `scan_dependencies` and `check_dependencies` tools query the [OSV API](https://osv.dev/) to check for known CVEs. This is opt-in -- you only call it when you explicitly use those tools. No other tool makes network requests.
366
+ - **Safe for air-gapped environments.** All code analysis rules run entirely offline. Only dependency vulnerability checks require network access.
367
+
368
+ ## Configuration (.guardviberc)
369
+
370
+ Create a `.guardviberc` JSON file in your project root to customize GuardVibe behavior.
371
+
372
+ ### Full example
373
+
374
+ ```json
375
+ {
376
+ "rules": {
377
+ "disable": ["VG030", "VG045"],
378
+ "severity": {
379
+ "VG002": "medium",
380
+ "VG010": "low"
381
+ }
382
+ },
383
+ "scan": {
384
+ "exclude": ["fixtures/", "coverage/", "dist/", "vendor/"],
385
+ "maxFileSize": 1048576
386
+ },
387
+ "plugins": [
388
+ "guardvibe-rules-awesome",
389
+ "./my-local-rules"
390
+ ],
391
+ "compliance": {
392
+ "frameworks": ["SOC2", "HIPAA"],
393
+ "failOn": "high",
394
+ "exceptions": [
395
+ {
396
+ "ruleId": "VG030",
397
+ "reason": "Accepted risk per security review 2026-03",
398
+ "approvedBy": "security-team",
399
+ "expiresAt": "2026-12-31",
400
+ "files": ["src/legacy/**"]
401
+ }
402
+ ],
403
+ "requiredControls": ["SOC2:CC6.1"]
404
+ }
405
+ }
406
+ ```
407
+
408
+ ### Configuration fields
409
+
410
+ | Field | Type | Default | Description |
411
+ |-------|------|---------|-------------|
412
+ | `rules.disable` | `string[]` | `[]` | Rule IDs to skip during scanning |
413
+ | `rules.severity` | `Record<string, string>` | `{}` | Override severity for specific rules |
414
+ | `scan.exclude` | `string[]` | `[]` | Glob patterns for directories/files to skip |
415
+ | `scan.maxFileSize` | `number` | `512000` | Maximum file size in bytes (files larger than this are skipped) |
416
+ | `plugins` | `string[]` | `[]` | npm package names or local paths to load as plugins |
417
+ | `compliance.frameworks` | `string[]` | -- | Compliance frameworks to map against (`SOC2`, `PCI-DSS`, `HIPAA`, `GDPR`, `ISO27001`) |
418
+ | `compliance.failOn` | `string` | `"high"` | Minimum severity that causes compliance failure |
419
+ | `compliance.exceptions` | `PolicyException[]` | `[]` | Approved exceptions with expiration dates |
420
+ | `compliance.requiredControls` | `string[]` | -- | Controls that must pass regardless of exceptions |
421
+
314
422
  ## Security
315
423
 
316
424
  GuardVibe takes supply chain security seriously:
package/build/cli.js CHANGED
@@ -1,7 +1,10 @@
1
1
  #!/usr/bin/env node
2
+ import { createRequire } from "module";
2
3
  import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync, unlinkSync } from "fs";
3
4
  import { join, dirname } from "path";
4
5
  import { homedir } from "os";
6
+ const require = createRequire(import.meta.url);
7
+ const pkg = require("../package.json");
5
8
  const GUARDVIBE_MCP_CONFIG = {
6
9
  command: "npx",
7
10
  args: ["-y", "guardvibe"],
@@ -314,23 +317,33 @@ function printUsage() {
314
317
  console.log(`
315
318
  GuardVibe Security - CLI
316
319
 
317
- Usage:
320
+ Commands:
318
321
  npx guardvibe scan [path] Scan a directory for security issues
319
- npx guardvibe check <file> Scan a single file
320
- npx guardvibe init <platform> Setup MCP server
321
- npx guardvibe hook install Install pre-commit hook
322
- npx guardvibe hook uninstall Remove pre-commit hook
322
+ npx guardvibe check <file> Scan a single file for security issues
323
+ npx guardvibe init <platform> Setup MCP server configuration
324
+ npx guardvibe hook install Install pre-commit security hook
325
+ npx guardvibe hook uninstall Remove pre-commit security hook
323
326
  npx guardvibe ci github Generate GitHub Actions workflow
324
327
 
328
+ Scan CLI (used by pre-commit hook and CI):
329
+ npx guardvibe-scan Scan git-staged files
330
+ npx guardvibe-scan --format sarif --output results.sarif
331
+
325
332
  Options:
326
333
  --format <type> Output format: markdown (default), json, sarif
327
334
  --output <file> Write results to file instead of stdout
335
+ --version, -V Print version and exit
336
+ --help, -h Show this help message
328
337
 
329
338
  MCP Platforms:
330
- claude Claude Code (.claude.json)
339
+ claude Claude Code (.claude.json in project root)
331
340
  gemini Gemini CLI (~/.gemini/settings.json)
332
341
  cursor Cursor (.cursor/mcp.json)
333
- all All platforms
342
+ all All platforms at once
343
+
344
+ Supported File Types:
345
+ .js .jsx .mjs .cjs .ts .tsx .mts .cts .py .go .html .sql
346
+ .sh .bash .yml .yaml .tf .toml .json Dockerfile
334
347
 
335
348
  Examples:
336
349
  npx guardvibe scan .
@@ -345,6 +358,10 @@ function printUsage() {
345
358
  }
346
359
  async function main() {
347
360
  const args = process.argv.slice(2);
361
+ if (args.includes("--version") || args.includes("-V")) {
362
+ console.log(pkg.version);
363
+ process.exit(0);
364
+ }
348
365
  if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
349
366
  printUsage();
350
367
  process.exit(0);
@@ -9,6 +9,7 @@ export const cicdRules = [
9
9
  languages: ["yaml"],
10
10
  fix: "Pass secrets as environment variables instead of interpolating in run steps.",
11
11
  fixCode: "# Pass secrets via env, not interpolation\nsteps:\n - run: echo \"deploying\"\n env:\n MY_SECRET: ${{ secrets.MY_SECRET }}",
12
+ compliance: ["SOC2:CC6.1", "PCI-DSS:Req2.3"],
12
13
  },
13
14
  {
14
15
  id: "VG211",
@@ -20,6 +21,7 @@ export const cicdRules = [
20
21
  languages: ["yaml"],
21
22
  fix: "Use pull_request trigger instead, or avoid checking out PR code with pull_request_target.",
22
23
  fixCode: "# Use pull_request instead of pull_request_target\non:\n pull_request:\n branches: [main]",
24
+ compliance: ["SOC2:CC6.1", "SOC2:CC6.6"],
23
25
  },
24
26
  {
25
27
  id: "VG212",
@@ -31,6 +33,7 @@ export const cicdRules = [
31
33
  languages: ["yaml"],
32
34
  fix: "Pin actions to a specific commit SHA or version tag.",
33
35
  fixCode: "# Pin to specific version or SHA\nuses: actions/checkout@v4\n# Or pin to commit SHA:\n# uses: actions/checkout@abc123def456",
36
+ compliance: ["SOC2:CC7.1"],
34
37
  },
35
38
  {
36
39
  id: "VG213",
@@ -42,6 +45,7 @@ export const cicdRules = [
42
45
  languages: ["yaml"],
43
46
  fix: "Specify minimum required permissions for each job.",
44
47
  fixCode: "# Use least-privilege permissions\npermissions:\n contents: read\n pull-requests: write",
48
+ compliance: ["SOC2:CC6.1", "PCI-DSS:Req8"],
45
49
  },
46
50
  {
47
51
  id: "VG214",
@@ -132,6 +132,7 @@ export const coreRules = [
132
132
  languages: ["json"],
133
133
  fix: "Pin dependencies to specific versions or use caret ranges (^1.2.3). Run npm audit regularly.",
134
134
  fixCode: "// Pin to specific version\n\"lodash\": \"^4.17.21\"\n// Run: npm audit to check for vulnerabilities",
135
+ compliance: ["SOC2:CC7.1"],
135
136
  },
136
137
  {
137
138
  id: "VG030",
@@ -143,6 +144,7 @@ export const coreRules = [
143
144
  languages: ["javascript", "typescript", "python", "go"],
144
145
  fix: "Add rate limiting middleware. Express: npm install express-rate-limit. FastAPI: use slowapi. Apply stricter limits on auth endpoints (e.g. 5 requests/minute).",
145
146
  fixCode: "// Express rate limiting\nimport rateLimit from 'express-rate-limit';\napp.use('/api/', rateLimit({ windowMs: 15 * 60 * 1000, max: 100 }));",
147
+ compliance: ["SOC2:CC7.1"],
146
148
  },
147
149
  {
148
150
  id: "VG040",
@@ -166,6 +168,7 @@ export const coreRules = [
166
168
  languages: ["javascript", "typescript", "python"],
167
169
  fix: "Disable debug mode in production. Never expose stack traces to users.",
168
170
  fixCode: "// Use environment-based config\nconst DEBUG = process.env.NODE_ENV !== 'production';",
171
+ compliance: ["SOC2:CC6.1"],
169
172
  },
170
173
  {
171
174
  id: "VG042",
@@ -177,6 +180,7 @@ export const coreRules = [
177
180
  languages: ["javascript", "typescript"],
178
181
  fix: "Use helmet middleware: npm install helmet, then app.use(helmet()).",
179
182
  fixCode: "// Add helmet for security headers\nimport helmet from 'helmet';\napp.use(helmet());",
183
+ compliance: ["SOC2:CC6.1"],
180
184
  },
181
185
  {
182
186
  id: "VG060",
@@ -205,7 +209,7 @@ export const coreRules = [
205
209
  {
206
210
  id: "VG062",
207
211
  name: "Hardcoded secret in variable",
208
- severity: "high",
212
+ severity: "critical",
209
213
  owasp: "A07:2025 Auth Failures",
210
214
  description: "Variable named secret, password, or apiKey assigned a string literal. Secrets should come from environment variables or a secrets manager, never hardcoded in source.",
211
215
  pattern: /(?:(?:const|let|var|export)\s+)?(?:secret|password|passwd|apiKey|api_key|privateKey|private_key|signingKey|signing_key|encryptionKey|encryption_key|masterKey|master_key|dbPassword|db_password)\s*(?::\s*string\s*)?=\s*["'][^"']{8,}["']/gi,
@@ -346,4 +350,40 @@ export const coreRules = [
346
350
  fixCode: '// BAD: user controls the regex\nconst re = new RegExp(req.query.search);\n\n// GOOD: escape regex special chars\nfunction escapeRegex(s: string) {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\\\$&");\n}\nconst re = new RegExp(escapeRegex(req.query.search));\n\n// BETTER: use string methods\nconst results = items.filter(i => i.name.includes(query));',
347
351
  compliance: ["SOC2:CC7.1"],
348
352
  },
353
+ {
354
+ id: "VG108",
355
+ name: "Vue v-html Directive with User Data",
356
+ severity: "high",
357
+ owasp: "A07:2025 Cross-Site Scripting",
358
+ description: "Vue's v-html directive renders raw HTML without sanitization, equivalent to innerHTML. If user-controlled data is bound via v-html, attackers can inject arbitrary scripts for stored or reflected XSS.",
359
+ pattern: /v-html\s*=\s*["'](?!['"])\w/gi,
360
+ languages: ["html", "javascript", "typescript"],
361
+ fix: "Avoid v-html with user data. Use text interpolation {{ }} or sanitize with DOMPurify before rendering.",
362
+ fixCode: '<!-- BAD: raw HTML rendering -->\n<!-- <div v-html="userComment"></div> -->\n\n<!-- GOOD: text interpolation (auto-escaped) -->\n<div>{{ userComment }}</div>\n\n<!-- If HTML needed: sanitize first -->\n<div v-html="DOMPurify.sanitize(userComment)"></div>',
363
+ compliance: ["SOC2:CC7.1"],
364
+ },
365
+ {
366
+ id: "VG109",
367
+ name: "Angular innerHTML Binding with User Data",
368
+ severity: "high",
369
+ owasp: "A07:2025 Cross-Site Scripting",
370
+ description: "Angular's [innerHTML] property binding renders HTML content. While Angular's built-in sanitizer strips scripts, it can be bypassed with bypassSecurityTrustHtml() or via CSS/SVG-based XSS vectors.",
371
+ pattern: /(?:\[innerHTML\]\s*=\s*["']\w|bypassSecurityTrustHtml\s*\()/gi,
372
+ languages: ["html", "typescript"],
373
+ fix: "Avoid [innerHTML] with user data. If unavoidable, never use bypassSecurityTrustHtml() on user input.",
374
+ fixCode: '<!-- BAD: bypass Angular sanitizer -->\n<!-- <div [innerHTML]="trustedHtml"></div> -->\n<!-- this.trustedHtml = this.sanitizer.bypassSecurityTrustHtml(userInput); -->\n\n<!-- GOOD: let Angular sanitize automatically -->\n<div [innerText]="userInput"></div>\n<!-- Or use Angular pipe with DOMPurify -->',
375
+ compliance: ["SOC2:CC7.1"],
376
+ },
377
+ {
378
+ id: "VG116",
379
+ name: "HTML Event Handler Injection via User Input",
380
+ severity: "high",
381
+ owasp: "A07:2025 Cross-Site Scripting",
382
+ description: "User input is interpolated into HTML attributes that accept JavaScript (onclick, onerror, onload, onmouseover, onfocus). Even without script tags, event handlers execute arbitrary JavaScript when the element is interacted with or loads.",
383
+ pattern: /(?:on(?:click|error|load|mouseover|focus|blur|submit|change|input|keyup|keydown))\s*=\s*(?:`[^`]*\$\{|["'][^"']*["']\s*\+\s*(?:user|input|query|param|req\.|data\.))/gi,
384
+ languages: ["javascript", "typescript", "html"],
385
+ fix: "Never interpolate user input into HTML event handler attributes. Use addEventListener with sanitized data instead.",
386
+ fixCode: '// BAD: user input in event handler\n// `<img onerror="${userInput}">`\n\n// GOOD: use addEventListener\nconst img = document.createElement("img");\nimg.addEventListener("error", () => handleError(sanitizedInput));',
387
+ compliance: ["SOC2:CC7.1"],
388
+ },
349
389
  ];
@@ -47,6 +47,7 @@ export const deploymentRules = [
47
47
  languages: ["vercel-config", "json"],
48
48
  fix: "Set maxDuration to the minimum required. Default 300s is sufficient for most use cases.",
49
49
  fixCode: '// Set reasonable maxDuration\nexport const maxDuration = 60; // seconds — adjust to actual need',
50
+ compliance: ["SOC2:CC6.1"],
50
51
  },
51
52
  {
52
53
  id: "VG506",
@@ -83,6 +84,7 @@ export const deploymentRules = [
83
84
  languages: ["nextjs-config", "javascript", "typescript"],
84
85
  fix: "Set poweredByHeader to false in next.config.ts.",
85
86
  fixCode: "// next.config.ts\nconst config = {\n poweredByHeader: false,\n};",
87
+ compliance: ["SOC2:CC6.1"],
86
88
  },
87
89
  {
88
90
  id: "VG510",
@@ -230,4 +232,16 @@ export const deploymentRules = [
230
232
  fixCode: '# BAD: breaks container isolation\n# hostNetwork: true\n# hostPID: true\n\n# GOOD: use pod networking\nspec:\n hostNetwork: false\n hostPID: false\n hostIPC: false\n containers:\n - name: app\n securityContext:\n runAsNonRoot: true',
231
233
  compliance: ["SOC2:CC6.1", "PCI-DSS:Req6.5.10"],
232
234
  },
235
+ {
236
+ id: "VG524",
237
+ name: "Data URL or Blob URL in User-Controlled src/href",
238
+ severity: "high",
239
+ owasp: "A07:2025 Cross-Site Scripting",
240
+ description: "User-controlled input is used in src or href attributes without blocking data: and blob: URL schemes. data:text/html URLs can embed full HTML pages with scripts, and javascript: URLs execute code — bypassing same-origin restrictions.",
241
+ pattern: /(?:src|href|action|poster|srcDoc)\s*=\s*\{[\s\S]{0,100}?(?:user|input|param|query|data|url|link|content)[\s\S]{0,100}?(?:(?!(?:startsWith|protocol|scheme|allowlist|whitelist|URL\()[\s\S]{0,50}?))\}/gi,
242
+ languages: ["javascript", "typescript"],
243
+ fix: "Validate URL scheme against an allowlist (https: only). Block data:, blob:, and javascript: URLs from user input.",
244
+ fixCode: '// Validate URL scheme\nfunction isSafeUrl(url: string): boolean {\n try {\n const parsed = new URL(url);\n return ["https:", "http:"].includes(parsed.protocol);\n } catch {\n return false;\n }\n}\n\n// Usage\n<img src={isSafeUrl(userUrl) ? userUrl : "/placeholder.png"} />',
245
+ compliance: ["SOC2:CC7.1"],
246
+ },
233
247
  ];
@@ -9,6 +9,7 @@ export const dockerfileRules = [
9
9
  languages: ["dockerfile"],
10
10
  fix: "Add a USER instruction after installing dependencies: USER node (or appropriate non-root user).",
11
11
  fixCode: "FROM node:20-alpine\nRUN addgroup -S app && adduser -S app -G app\n# ... install dependencies ...\nUSER app\nCMD [\"node\", \"server.js\"]",
12
+ compliance: ["SOC2:CC6.1"],
12
13
  },
13
14
  {
14
15
  id: "VG201",
@@ -20,6 +21,7 @@ export const dockerfileRules = [
20
21
  languages: ["dockerfile"],
21
22
  fix: "Copy only dependency files first (package.json, requirements.txt), then install, then copy source.",
22
23
  fixCode: "# Copy dependency files first\nCOPY package.json package-lock.json ./\nRUN npm ci --production\n# Then copy source\nCOPY . .",
24
+ compliance: ["SOC2:CC6.1"],
23
25
  },
24
26
  {
25
27
  id: "VG202",
@@ -31,6 +33,7 @@ export const dockerfileRules = [
31
33
  languages: ["dockerfile"],
32
34
  fix: "Pin to a specific version tag: FROM node:20-alpine instead of FROM node:latest.",
33
35
  fixCode: "# Pin to specific version\nFROM node:20-alpine\n# Not: FROM node:latest\n# Not: FROM node",
36
+ compliance: ["SOC2:CC7.1"],
34
37
  },
35
38
  {
36
39
  id: "VG203",
@@ -42,6 +45,7 @@ export const dockerfileRules = [
42
45
  languages: ["dockerfile"],
43
46
  fix: "Use runtime environment variables or Docker secrets instead of baking secrets into the image.",
44
47
  fixCode: "# Don't bake secrets in image\n# Instead, pass at runtime:\n# docker run -e SECRET_KEY=xxx myapp\n# Or use Docker secrets / .env file",
48
+ compliance: ["SOC2:CC6.1", "PCI-DSS:Req2.3"],
45
49
  },
46
50
  {
47
51
  id: "VG204",
@@ -53,6 +57,7 @@ export const dockerfileRules = [
53
57
  languages: ["dockerfile"],
54
58
  fix: "Use COPY instead of ADD for local files. Only use ADD for URLs or tar extraction.",
55
59
  fixCode: "# Use COPY for local files\nCOPY ./src /app/src\n# Only use ADD for remote files or tar extraction\n# ADD https://example.com/file.tar.gz /app/",
60
+ compliance: ["SOC2:CC6.1"],
56
61
  },
57
62
  {
58
63
  id: "VG205",
@@ -10,6 +10,7 @@ export const goRules = [
10
10
  languages: ["go"],
11
11
  fix: "Use parameterized queries: db.Query('SELECT * FROM users WHERE id = $1', id). Never use fmt.Sprintf for SQL.",
12
12
  fixCode: "// Use parameterized queries\nrows, err := db.Query(\"SELECT * FROM users WHERE id = $1\", id)",
13
+ compliance: ["SOC2:CC7.1", "PCI-DSS:Req6.5.1"],
13
14
  },
14
15
  {
15
16
  id: "VG111",
@@ -21,6 +22,7 @@ export const goRules = [
21
22
  languages: ["go"],
22
23
  fix: "Validate and sanitize all input before passing to exec.Command. Use an allowlist of permitted commands.",
23
24
  fixCode: "// Validate input, use allowlist\ncmd := exec.Command(\"ls\", \"-la\", safeDir)",
25
+ compliance: ["SOC2:CC7.1", "PCI-DSS:Req6.5.1"],
24
26
  },
25
27
  {
26
28
  id: "VG112",
@@ -32,6 +34,7 @@ export const goRules = [
32
34
  languages: ["go"],
33
35
  fix: "Avoid template.HTML() with user input. Use html/template which auto-escapes by default.",
34
36
  fixCode: "// Use html/template which auto-escapes\n// Avoid template.HTML() with user input\ntmpl.Execute(w, data) // auto-escaped",
37
+ compliance: ["SOC2:CC7.1", "PCI-DSS:Req6.5.7"],
35
38
  },
36
39
  {
37
40
  id: "VG113",
@@ -43,6 +46,7 @@ export const goRules = [
43
46
  languages: ["go"],
44
47
  fix: "Wrap handlers with authentication middleware: http.Handle('/api/', authMiddleware(handler)).",
45
48
  fixCode: "// Wrap with auth middleware\nhttp.Handle(\"/api/\", authMiddleware(apiHandler))",
49
+ compliance: ["SOC2:CC6.1", "SOC2:CC6.6"],
46
50
  },
47
51
  {
48
52
  id: "VG114",
@@ -54,6 +58,7 @@ export const goRules = [
54
58
  languages: ["go"],
55
59
  fix: "Use crypto/sha256 or golang.org/x/crypto/bcrypt for password hashing.",
56
60
  fixCode: "// Use bcrypt for passwords\nimport \"golang.org/x/crypto/bcrypt\"\nhash, _ := bcrypt.GenerateFromPassword([]byte(password), 12)",
61
+ compliance: ["SOC2:CC6.1", "PCI-DSS:Req4.1"],
57
62
  },
58
63
  {
59
64
  id: "VG115",
@@ -65,5 +70,6 @@ export const goRules = [
65
70
  languages: ["go"],
66
71
  fix: "Set specific allowed origins instead of wildcard '*'.",
67
72
  fixCode: "// Specify allowed origins\nw.Header().Set(\"Access-Control-Allow-Origin\", \"https://myapp.com\")",
73
+ compliance: ["SOC2:CC6.1"],
68
74
  },
69
75
  ];
@@ -434,4 +434,52 @@ export const modernStackRules = [
434
434
  fixCode: '// Express: trust only your reverse proxy\napp.set("trust proxy", 1); // trust first proxy\n\n// Rate limiter: use req.ip (respects trust proxy)\nimport rateLimit from "express-rate-limit";\nconst limiter = rateLimit({\n keyGenerator: (req) => req.ip, // uses trusted proxy chain\n max: 100,\n windowMs: 15 * 60 * 1000,\n});',
435
435
  compliance: ["SOC2:CC7.1"],
436
436
  },
437
+ {
438
+ id: "VG990",
439
+ name: "SVG File Upload Without Content Sanitization",
440
+ severity: "critical",
441
+ owasp: "A07:2025 Cross-Site Scripting",
442
+ description: "File upload accepts SVG files but does not scan or sanitize SVG content. SVG files can contain embedded <script> tags, event handlers (onload, onclick), and external resource references that execute JavaScript when the SVG is rendered in a browser.",
443
+ pattern: /(?:(?:allowedMimeTypes|accept|mimeTypes|fileTypes|allowedTypes|contentType)\s*[:=]\s*(?:\[[\s\S]*?|['"`])[\s\S]{0,100}?(?:svg|image\/svg|\.svg))/gi,
444
+ languages: ["javascript", "typescript"],
445
+ fix: "Either reject SVG uploads entirely or sanitize SVG content by stripping script tags, event handlers, and external references. Use a library like DOMPurify with SVG profile.",
446
+ fixCode: '// Option 1: Reject SVGs\nconst ALLOWED_TYPES = ["image/png", "image/jpeg", "image/webp"]; // no SVG\n\n// Option 2: Sanitize SVG content\nimport DOMPurify from "dompurify";\nconst cleanSvg = DOMPurify.sanitize(svgContent, {\n USE_PROFILES: { svg: true, svgFilters: true },\n FORBID_TAGS: ["script", "foreignObject"],\n FORBID_ATTR: ["onclick", "onerror", "onload"],\n});',
447
+ compliance: ["SOC2:CC7.1"],
448
+ },
449
+ {
450
+ id: "VG991",
451
+ name: "Markdown Rendered as HTML Without Sanitization",
452
+ severity: "high",
453
+ owasp: "A07:2025 Cross-Site Scripting",
454
+ description: "Markdown library output (marked, showdown, markdown-it, remark) is rendered as HTML without sanitization. Most markdown parsers allow raw HTML by default — user-submitted markdown like `<img onerror=alert(1)>` passes through as executable HTML.",
455
+ pattern: /(?:marked|showdown|markdownIt|markdown-it|unified|remark|rehype)[\s\S]{0,300}?(?:innerHTML|dangerouslySetInnerHTML|v-html|\[innerHTML\]|\.html\s*\(|res\.send)/gi,
456
+ languages: ["javascript", "typescript"],
457
+ fix: "Sanitize markdown HTML output with DOMPurify before rendering, or configure the parser to disable raw HTML.",
458
+ fixCode: '// BAD: unsanitized markdown\n// element.innerHTML = marked.parse(userMarkdown);\n\n// GOOD: sanitize after parsing\nimport DOMPurify from "dompurify";\nimport { marked } from "marked";\nconst html = DOMPurify.sanitize(marked.parse(userMarkdown));\n\n// Or disable HTML in parser\nmarked.setOptions({ sanitize: true });\n// markdown-it: const md = markdownIt({ html: false });',
459
+ compliance: ["SOC2:CC7.1"],
460
+ },
461
+ {
462
+ id: "VG992",
463
+ name: "Rich Text Editor Output Without Sanitization",
464
+ severity: "high",
465
+ owasp: "A07:2025 Cross-Site Scripting",
466
+ description: "WYSIWYG/rich text editor content (TipTap, Draft.js, Slate, Quill, CKEditor, TinyMCE) is rendered via innerHTML or dangerouslySetInnerHTML without sanitization. Editor output is user-controlled HTML that can contain XSS payloads — especially if the editor allows source code editing or paste from external sources.",
467
+ pattern: /(?:editor\.getHTML|getContent|convertToHTML|stateToHTML|serialize|draftToHtml|renderToString)[\s\S]{0,300}?(?:innerHTML|dangerouslySetInnerHTML|v-html|\[innerHTML\])/gi,
468
+ languages: ["javascript", "typescript"],
469
+ fix: "Always sanitize rich text editor output with DOMPurify before rendering, even if the editor has its own sanitization.",
470
+ fixCode: '// BAD: direct editor output rendering\n// <div dangerouslySetInnerHTML={{ __html: editor.getHTML() }} />\n\n// GOOD: sanitize editor output\nimport DOMPurify from "dompurify";\nconst cleanHtml = DOMPurify.sanitize(editor.getHTML(), {\n ALLOWED_TAGS: ["p", "b", "i", "em", "strong", "a", "ul", "ol", "li", "br", "h1", "h2", "h3"],\n ALLOWED_ATTR: ["href", "target", "rel"],\n});\n<div dangerouslySetInnerHTML={{ __html: cleanHtml }} />',
471
+ compliance: ["SOC2:CC7.1"],
472
+ },
473
+ {
474
+ id: "VG993",
475
+ name: "Upload Filename Used Without Sanitization",
476
+ severity: "high",
477
+ owasp: "A01:2025 Broken Access Control",
478
+ description: "User-uploaded file's original filename is used directly for storage without sanitization. Attackers can use directory traversal (../../etc/passwd), null bytes (file.php%00.jpg), double extensions (file.jpg.exe), or Unicode tricks to overwrite files, bypass type checks, or achieve remote code execution.",
479
+ pattern: /(?:file\.name|originalname|filename|req\.file\.originalname|formData\.get\s*\(\s*['"]file['"])\s*[\s\S]{0,100}?(?:writeFile|createWriteStream|save|upload|putObject|mv\s*\(|rename|storage)/gi,
480
+ languages: ["javascript", "typescript"],
481
+ fix: "Generate a random filename (UUID/nanoid) and validate the extension against an allowlist. Never use the original filename for storage.",
482
+ fixCode: 'import { randomUUID } from "crypto";\nimport path from "path";\n\n// Generate safe filename\nconst ext = path.extname(file.name).toLowerCase();\nconst ALLOWED_EXT = [".jpg", ".jpeg", ".png", ".webp", ".pdf"];\nif (!ALLOWED_EXT.includes(ext)) throw new Error("Invalid file type");\nconst safeName = `${randomUUID()}${ext}`;\nawait fs.writeFile(`/uploads/${safeName}`, buffer);',
483
+ compliance: ["SOC2:CC7.1"],
484
+ },
437
485
  ];
@@ -168,4 +168,16 @@ export const nextjsRules = [
168
168
  fixCode: '// next.config.ts\nexport default {\n serverActions: {\n allowedOrigins: ["myapp.com", "*.myapp.com"],\n },\n};',
169
169
  compliance: ["SOC2:CC6.6"],
170
170
  },
171
+ {
172
+ id: "VG414",
173
+ name: "Server-Side Template Injection (SSTI)",
174
+ severity: "critical",
175
+ owasp: "A02:2025 Injection",
176
+ description: "User input is rendered using an unescaped template directive (EJS <%- %>, Handlebars {{{ }}}, Pug != operator, Nunjucks | safe filter). These directives bypass HTML escaping, allowing attackers to inject arbitrary HTML and JavaScript that executes server-side or client-side.",
177
+ pattern: /(?:<%-\s*\w|(?:\{\{\{)\s*\w|!=\s*\w[\s\S]{0,50}?(?:user|input|query|body|param|req\.|data\.)|\|\s*safe\s*(?:\}\}|\%\}))/gi,
178
+ languages: ["javascript", "typescript", "html"],
179
+ fix: "Always use escaped template directives: EJS <%= %>, Handlebars {{ }}, Pug =. Only use unescaped rendering for trusted, developer-controlled content.",
180
+ fixCode: '<!-- EJS: use escaped output -->\n<p><%= userInput %></p> <!-- SAFE: HTML-escaped -->\n<!-- NOT: <%- userInput %> DANGEROUS: raw HTML -->\n\n<!-- Handlebars: use double braces -->\n<p>{{userInput}}</p> <!-- SAFE: escaped -->\n<!-- NOT: {{{userInput}}} DANGEROUS: raw HTML -->\n\n<!-- Pug: use = not != -->\np= userInput //- SAFE: escaped\n//- NOT: p!= userInput DANGEROUS: raw HTML',
181
+ compliance: ["SOC2:CC7.1", "PCI-DSS:Req6.5.1"],
182
+ },
171
183
  ];
@@ -173,4 +173,16 @@ export const webSecurityRules = [
173
173
  fixCode: "// Reads from OPENAI_API_KEY env automatically\nconst openai = new OpenAI();",
174
174
  compliance: ["SOC2:CC6.1"],
175
175
  },
176
+ {
177
+ id: "VG678",
178
+ name: "Missing X-Content-Type-Options Header",
179
+ severity: "high",
180
+ owasp: "A05:2021 Security Misconfiguration",
181
+ description: "Response serving user-uploaded files does not set X-Content-Type-Options: nosniff. Browsers may MIME-sniff the content and execute uploaded files as HTML/JavaScript, enabling stored XSS via file uploads.",
182
+ pattern: /(?:createReadStream|sendFile|send\s*\(|pipe\s*\(|res\.download|res\.sendFile|getSignedUrl|getPublicUrl)[\s\S]{0,500}?(?:(?!X-Content-Type-Options|nosniff)[\s\S]){10,}?(?:res\.end|\.pipe|return|response)/gi,
183
+ languages: ["javascript", "typescript"],
184
+ fix: "Set X-Content-Type-Options: nosniff on all responses serving user-uploaded content.",
185
+ fixCode: '// Set nosniff header for uploaded file responses\nres.setHeader("X-Content-Type-Options", "nosniff");\nres.setHeader("Content-Disposition", "attachment"); // force download for unknown types\nres.sendFile(filePath);',
186
+ compliance: ["SOC2:CC6.1"],
187
+ },
176
188
  ];
package/build/index.js CHANGED
@@ -1,8 +1,11 @@
1
1
  #!/usr/bin/env node
2
+ import { createRequire } from "module";
2
3
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
4
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
5
  import { z } from "zod";
5
6
  import { checkCode } from "./tools/check-code.js";
7
+ const require = createRequire(import.meta.url);
8
+ const pkg = require("../package.json");
6
9
  import { checkProject } from "./tools/check-project.js";
7
10
  import { getSecurityDocs } from "./tools/get-security-docs.js";
8
11
  import { checkDependencies } from "./tools/check-deps.js";
@@ -27,9 +30,10 @@ import { explainRemediation } from "./tools/explain-remediation.js";
27
30
  import { discoverPlugins } from "./plugins/loader.js";
28
31
  import { builtinRules } from "./data/rules/index.js";
29
32
  import { loadConfig } from "./utils/config.js";
33
+ import { setRules, getRules } from "./utils/rule-registry.js";
30
34
  const server = new McpServer({
31
35
  name: "guardvibe",
32
- version: "1.6.0",
36
+ version: pkg.version,
33
37
  });
34
38
  // Tool 1: Analyze code for security vulnerabilities
35
39
  server.tool("check_code", "Analyze code for security vulnerabilities (OWASP Top 10, XSS, SQL injection, insecure patterns). Use this when reviewing or writing code to catch security issues early.", {
@@ -43,7 +47,7 @@ server.tool("check_code", "Analyze code for security vulnerabilities (OWASP Top
43
47
  .describe("Framework context (e.g. express, nextjs, fastapi, react, django)"),
44
48
  format: z.enum(["markdown", "json"]).default("markdown").describe("Output format: markdown (human) or json (machine-readable for agents)"),
45
49
  }, async ({ code, language, framework, format }) => {
46
- const rules = globalThis.__guardvibe_rules;
50
+ const rules = getRules();
47
51
  const results = checkCode(code, language, framework, undefined, undefined, format, rules);
48
52
  return {
49
53
  content: [{ type: "text", text: results }],
@@ -59,7 +63,7 @@ server.tool("check_project", "Scan multiple files for security vulnerabilities a
59
63
  .describe("List of files to scan: [{path, content}]"),
60
64
  format: z.enum(["markdown", "json"]).default("markdown").describe("Output format: markdown (human) or json (machine-readable for agents)"),
61
65
  }, async ({ files, format }) => {
62
- const rules = globalThis.__guardvibe_rules;
66
+ const rules = getRules();
63
67
  const results = checkProject(files, format, rules);
64
68
  return {
65
69
  content: [{ type: "text", text: results }],
@@ -111,7 +115,7 @@ server.tool("scan_directory", "Scan an entire project directory for security vul
111
115
  format: z.enum(["markdown", "json"]).default("markdown").describe("Output format: markdown (human) or json (machine-readable for agents)"),
112
116
  baseline: z.string().optional().describe("Path to a previous scan JSON output file for baseline comparison (new/fixed/unchanged findings)"),
113
117
  }, async ({ path, recursive, exclude, format, baseline }) => {
114
- const rules = globalThis.__guardvibe_rules;
118
+ const rules = getRules();
115
119
  const results = scanDirectory(path, recursive, exclude, format, rules, baseline);
116
120
  return { content: [{ type: "text", text: results }] };
117
121
  });
@@ -136,7 +140,7 @@ server.tool("scan_secrets", "Scan files and directories for leaked secrets, API
136
140
  server.tool("scan_staged", "Scan git-staged files for security vulnerabilities before committing. Run this before every commit to catch issues early. No input needed — automatically reads staged files.", {
137
141
  format: z.enum(["markdown", "json"]).default("markdown").describe("Output format: markdown (human) or json (machine-readable for agents)"),
138
142
  }, async ({ format }) => {
139
- const rules = globalThis.__guardvibe_rules;
143
+ const rules = getRules();
140
144
  const results = scanStaged(process.cwd(), format, rules);
141
145
  return { content: [{ type: "text", text: results }] };
142
146
  });
@@ -147,7 +151,7 @@ server.tool("compliance_report", "Generate a compliance-focused security report
147
151
  format: z.enum(["markdown", "json"]).default("markdown").describe("Output format: markdown (human) or json (machine-readable for agents)"),
148
152
  mode: z.enum(["full", "executive"]).default("full").describe("Report mode: full (detailed) or executive (C-level summary)"),
149
153
  }, async ({ path, framework, format, mode }) => {
150
- const rules = globalThis.__guardvibe_rules;
154
+ const rules = getRules();
151
155
  const results = complianceReport(path, framework, format, rules, mode);
152
156
  return { content: [{ type: "text", text: results }] };
153
157
  });
@@ -155,7 +159,7 @@ server.tool("compliance_report", "Generate a compliance-focused security report
155
159
  server.tool("export_sarif", "Scan a directory and export results in SARIF v2.1.0 format for CI/CD integration (GitHub, GitLab, Azure DevOps). Returns JSON string.", {
156
160
  path: z.string().describe("Directory to scan"),
157
161
  }, async ({ path }) => {
158
- const rules = globalThis.__guardvibe_rules;
162
+ const rules = getRules();
159
163
  const results = exportSarif(path, rules);
160
164
  return { content: [{ type: "text", text: results }] };
161
165
  });
@@ -179,7 +183,7 @@ server.tool("fix_code", "Analyze code for security vulnerabilities and return fi
179
183
  .describe("Framework context (e.g. express, nextjs, fastapi, react, django)"),
180
184
  format: z.enum(["markdown", "json"]).default("json").describe("Output format: json (for agent auto-fix) or markdown (human review)"),
181
185
  }, async ({ code, language, framework, format }) => {
182
- const rules = globalThis.__guardvibe_rules;
186
+ const rules = getRules();
183
187
  const results = fixCode(code, language, framework, undefined, format, rules);
184
188
  return {
185
189
  content: [{ type: "text", text: results }],
@@ -209,7 +213,7 @@ server.tool("review_pr", "Review a pull request for security issues. Scans only
209
213
  diff_only: z.boolean().default(true).describe("Only report findings in changed lines (true) or all findings in changed files (false)"),
210
214
  fail_on: z.enum(["critical", "high", "medium", "low", "none"]).default("high").describe("Block PR if findings at this severity or above exist"),
211
215
  }, async ({ path, base, format, diff_only, fail_on }) => {
212
- const rules = globalThis.__guardvibe_rules;
216
+ const rules = getRules();
213
217
  const results = reviewPr(path, base, format, diff_only, fail_on, rules);
214
218
  return { content: [{ type: "text", text: results }] };
215
219
  });
@@ -227,7 +231,7 @@ server.tool("policy_check", "Check project against compliance policies defined i
227
231
  path: z.string().describe("Project root directory"),
228
232
  format: z.enum(["markdown", "json"]).default("markdown").describe("Output format"),
229
233
  }, async ({ path, format }) => {
230
- const rules = globalThis.__guardvibe_rules;
234
+ const rules = getRules();
231
235
  const results = policyCheck(path, format, rules);
232
236
  return { content: [{ type: "text", text: results }] };
233
237
  });
@@ -280,7 +284,7 @@ server.tool("explain_remediation", "Deep explanation of a security finding: why
280
284
  code: z.string().optional().describe("Affected code snippet for context"),
281
285
  format: z.enum(["markdown", "json"]).default("markdown").describe("Output format"),
282
286
  }, async ({ rule_id, code, format }) => {
283
- const rules = globalThis.__guardvibe_rules;
287
+ const rules = getRules();
284
288
  const results = explainRemediation(rule_id, code, format, rules);
285
289
  return { content: [{ type: "text", text: results }] };
286
290
  });
@@ -299,12 +303,18 @@ async function main() {
299
303
  // Register plugin tools
300
304
  for (const tool of plugins.tools) {
301
305
  server.tool(tool.name, tool.description, tool.schema, async (input) => {
302
- const result = await tool.handler(input);
303
- return { content: [{ type: "text", text: result }] };
306
+ try {
307
+ const result = await tool.handler(input);
308
+ return { content: [{ type: "text", text: result }] };
309
+ }
310
+ catch (err) {
311
+ const msg = err instanceof Error ? err.message : String(err);
312
+ return { content: [{ type: "text", text: `Plugin error (${tool.name}): ${msg}` }] };
313
+ }
304
314
  });
305
315
  }
306
316
  // Store merged rules for tool handlers
307
- globalThis.__guardvibe_rules = allRules;
317
+ setRules(allRules);
308
318
  const transport = new StdioServerTransport();
309
319
  await server.connect(transport);
310
320
  console.error("GuardVibe Security MCP server running on stdio");
@@ -1,59 +1,15 @@
1
- import { readdirSync, readFileSync, statSync } from "fs";
2
- import { join, extname, basename, resolve } from "path";
1
+ import { readFileSync, statSync } from "fs";
2
+ import { extname, basename, resolve } from "path";
3
3
  import { analyzeCode } from "./check-code.js";
4
4
  import { loadConfig } from "../utils/config.js";
5
- const EXTENSION_MAP = {
6
- ".js": "javascript", ".jsx": "javascript", ".mjs": "javascript", ".cjs": "javascript",
7
- ".ts": "typescript", ".tsx": "typescript", ".mts": "typescript", ".cts": "typescript",
8
- ".py": "python", ".go": "go", ".html": "html",
9
- ".sql": "sql", ".sh": "shell", ".bash": "shell",
10
- ".yml": "yaml", ".yaml": "yaml", ".tf": "terraform",
11
- ".toml": "toml", ".json": "json",
12
- };
13
- const CONFIG_FILE_MAP = {
14
- "vercel.json": "vercel-config",
15
- "next.config.js": "nextjs-config",
16
- "next.config.mjs": "nextjs-config",
17
- "next.config.ts": "nextjs-config",
18
- "docker-compose.yml": "docker-compose",
19
- "docker-compose.yaml": "docker-compose",
20
- "fly.toml": "fly-config",
21
- "render.yaml": "render-config",
22
- "netlify.toml": "netlify-config",
23
- };
24
- const DEFAULT_EXCLUDES = new Set([
25
- "node_modules", ".git", "build", "dist", "vendor", "__pycache__",
26
- ".next", ".nuxt", "coverage", ".turbo",
27
- ]);
28
- function walkDir(dir, excludes, results) {
29
- let entries;
30
- try {
31
- entries = readdirSync(dir, { withFileTypes: true });
32
- }
33
- catch {
34
- return;
35
- }
36
- for (const entry of entries) {
37
- if (excludes.has(entry.name))
38
- continue;
39
- const fullPath = join(dir, entry.name);
40
- if (entry.isDirectory()) {
41
- walkDir(fullPath, excludes, results);
42
- }
43
- else if (entry.isFile()) {
44
- const ext = extname(entry.name).toLowerCase();
45
- if (EXTENSION_MAP[ext] || entry.name.startsWith("Dockerfile") || CONFIG_FILE_MAP[entry.name]) {
46
- results.push(fullPath);
47
- }
48
- }
49
- }
50
- }
5
+ import { EXTENSION_MAP, CONFIG_FILE_MAP, DEFAULT_EXCLUDES } from "../utils/constants.js";
6
+ import { walkDirectory } from "../utils/walk-directory.js";
51
7
  export function complianceReport(path, framework, format = "markdown", rules, mode = "full") {
52
8
  const scanRoot = resolve(path);
53
9
  const config = loadConfig(scanRoot);
54
10
  const excludes = new Set([...DEFAULT_EXCLUDES, ...config.scan.exclude]);
55
11
  const filePaths = [];
56
- walkDir(scanRoot, excludes, filePaths);
12
+ walkDirectory(scanRoot, true, excludes, filePaths);
57
13
  // Scan all files
58
14
  const allFindings = [];
59
15
  for (const filePath of filePaths) {
@@ -1,42 +1,10 @@
1
- import { readdirSync, readFileSync, statSync } from "fs";
2
- import { join, extname, basename, resolve } from "path";
1
+ import { readFileSync, statSync } from "fs";
2
+ import { extname, basename, resolve } from "path";
3
3
  import { analyzeCode } from "./check-code.js";
4
4
  import { owaspRules } from "../data/rules/index.js";
5
5
  import { loadConfig } from "../utils/config.js";
6
- const EXTENSION_MAP = {
7
- ".js": "javascript", ".jsx": "javascript", ".mjs": "javascript", ".cjs": "javascript",
8
- ".ts": "typescript", ".tsx": "typescript", ".mts": "typescript", ".cts": "typescript",
9
- ".py": "python", ".go": "go", ".html": "html",
10
- ".sql": "sql", ".sh": "shell", ".bash": "shell",
11
- ".yml": "yaml", ".yaml": "yaml", ".tf": "terraform", ".json": "json",
12
- };
13
- const DEFAULT_EXCLUDES = new Set([
14
- "node_modules", ".git", "build", "dist", "vendor", "__pycache__",
15
- ".next", ".nuxt", "coverage", ".turbo",
16
- ]);
17
- function walkDir(dir, excludes, results) {
18
- let entries;
19
- try {
20
- entries = readdirSync(dir, { withFileTypes: true });
21
- }
22
- catch {
23
- return;
24
- }
25
- for (const entry of entries) {
26
- if (excludes.has(entry.name))
27
- continue;
28
- const fullPath = join(dir, entry.name);
29
- if (entry.isDirectory()) {
30
- walkDir(fullPath, excludes, results);
31
- }
32
- else if (entry.isFile()) {
33
- const ext = extname(entry.name).toLowerCase();
34
- if (EXTENSION_MAP[ext] || entry.name.startsWith("Dockerfile")) {
35
- results.push(fullPath);
36
- }
37
- }
38
- }
39
- }
6
+ import { EXTENSION_MAP, DEFAULT_EXCLUDES } from "../utils/constants.js";
7
+ import { walkDirectory } from "../utils/walk-directory.js";
40
8
  function severityToLevel(severity) {
41
9
  if (severity === "critical" || severity === "high")
42
10
  return "error";
@@ -49,7 +17,7 @@ export function exportSarif(path, rules) {
49
17
  const config = loadConfig(scanRoot);
50
18
  const excludes = new Set([...DEFAULT_EXCLUDES, ...config.scan.exclude]);
51
19
  const filePaths = [];
52
- walkDir(scanRoot, excludes, filePaths);
20
+ walkDirectory(scanRoot, true, excludes, filePaths);
53
21
  const allResults = [];
54
22
  for (const filePath of filePaths) {
55
23
  try {
@@ -1,46 +1,9 @@
1
- import { readdirSync, readFileSync, statSync } from "fs";
2
- import { join, extname, basename, resolve } from "path";
1
+ import { readFileSync, statSync } from "fs";
2
+ import { extname, basename, resolve } from "path";
3
3
  import { analyzeCode } from "./check-code.js";
4
4
  import { loadConfig } from "../utils/config.js";
5
- const EXTENSION_MAP = {
6
- ".js": "javascript", ".jsx": "javascript", ".mjs": "javascript", ".cjs": "javascript",
7
- ".ts": "typescript", ".tsx": "typescript", ".mts": "typescript", ".cts": "typescript",
8
- ".py": "python", ".go": "go", ".html": "html",
9
- ".sql": "sql", ".sh": "shell", ".bash": "shell",
10
- ".yml": "yaml", ".yaml": "yaml", ".tf": "terraform",
11
- ".toml": "toml", ".json": "json",
12
- };
13
- const CONFIG_FILE_MAP = {
14
- "vercel.json": "vercel-config",
15
- "next.config.js": "nextjs-config", "next.config.mjs": "nextjs-config", "next.config.ts": "nextjs-config",
16
- "docker-compose.yml": "docker-compose", "docker-compose.yaml": "docker-compose",
17
- };
18
- const DEFAULT_EXCLUDES = new Set([
19
- "node_modules", ".git", "build", "dist", "vendor", "__pycache__",
20
- ".next", ".nuxt", "coverage", ".turbo",
21
- ]);
22
- function walkDir(dir, excludes, results) {
23
- let entries;
24
- try {
25
- entries = readdirSync(dir, { withFileTypes: true });
26
- }
27
- catch {
28
- return;
29
- }
30
- for (const entry of entries) {
31
- if (excludes.has(entry.name))
32
- continue;
33
- const fullPath = join(dir, entry.name);
34
- if (entry.isDirectory())
35
- walkDir(fullPath, excludes, results);
36
- else if (entry.isFile()) {
37
- const ext = extname(entry.name).toLowerCase();
38
- if (EXTENSION_MAP[ext] || entry.name.startsWith("Dockerfile") || CONFIG_FILE_MAP[entry.name]) {
39
- results.push(fullPath);
40
- }
41
- }
42
- }
43
- }
5
+ import { EXTENSION_MAP, CONFIG_FILE_MAP, DEFAULT_EXCLUDES } from "../utils/constants.js";
6
+ import { walkDirectory } from "../utils/walk-directory.js";
44
7
  function isExcepted(ruleId, filePath, exceptions) {
45
8
  for (const exc of exceptions) {
46
9
  if (exc.ruleId !== ruleId && exc.ruleId !== "*")
@@ -87,7 +50,7 @@ export function policyCheck(path, format = "markdown", rules) {
87
50
  }
88
51
  const excludes = new Set([...DEFAULT_EXCLUDES, ...config.scan.exclude]);
89
52
  const filePaths = [];
90
- walkDir(scanRoot, excludes, filePaths);
53
+ walkDirectory(scanRoot, true, excludes, filePaths);
91
54
  const policyFindings = [];
92
55
  const severityOrder = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
93
56
  const failLevel = severityOrder[policy.failOn] ?? 1;
@@ -1,19 +1,7 @@
1
1
  import { execFileSync } from "child_process";
2
2
  import { extname, basename } from "path";
3
3
  import { analyzeCode } from "./check-code.js";
4
- const EXTENSION_MAP = {
5
- ".js": "javascript", ".jsx": "javascript", ".mjs": "javascript", ".cjs": "javascript",
6
- ".ts": "typescript", ".tsx": "typescript", ".mts": "typescript", ".cts": "typescript",
7
- ".py": "python", ".go": "go", ".html": "html",
8
- ".sql": "sql", ".sh": "shell", ".bash": "shell",
9
- ".yml": "yaml", ".yaml": "yaml", ".tf": "terraform",
10
- ".toml": "toml", ".json": "json",
11
- };
12
- const CONFIG_FILE_MAP = {
13
- "vercel.json": "vercel-config",
14
- "next.config.js": "nextjs-config", "next.config.mjs": "nextjs-config", "next.config.ts": "nextjs-config",
15
- "docker-compose.yml": "docker-compose", "docker-compose.yaml": "docker-compose",
16
- };
4
+ import { EXTENSION_MAP, CONFIG_FILE_MAP } from "../utils/constants.js";
17
5
  function execGit(args, cwd) {
18
6
  try {
19
7
  return execFileSync("git", args, { cwd, encoding: "utf-8", timeout: 15000 });
@@ -1,64 +1,15 @@
1
- import { readdirSync, readFileSync, statSync } from "fs";
2
- import { join, extname, basename, resolve } from "path";
1
+ import { createRequire } from "module";
2
+ import { readFileSync, statSync } from "fs";
3
+ import { extname, basename, resolve } from "path";
3
4
  import { createHash, randomUUID } from "crypto";
4
5
  import { analyzeCode } from "./check-code.js";
5
6
  import { loadConfig } from "../utils/config.js";
6
- const DEFAULT_EXCLUDES = new Set([
7
- "node_modules", ".git", "build", "dist", "vendor", "__pycache__",
8
- ".next", ".nuxt", ".svelte-kit", "target", "bin", "obj",
9
- "coverage", ".turbo", ".venv", "venv",
10
- ]);
11
- const EXTENSION_MAP = {
12
- ".js": "javascript", ".jsx": "javascript", ".mjs": "javascript", ".cjs": "javascript",
13
- ".ts": "typescript", ".tsx": "typescript", ".mts": "typescript", ".cts": "typescript",
14
- ".py": "python", ".go": "go", ".html": "html",
15
- ".sql": "sql", ".sh": "shell", ".bash": "shell",
16
- ".yml": "yaml", ".yaml": "yaml",
17
- ".tf": "terraform",
18
- ".toml": "toml", ".json": "json",
19
- };
20
- const CONFIG_FILE_MAP = {
21
- "vercel.json": "vercel-config",
22
- "next.config.js": "nextjs-config",
23
- "next.config.mjs": "nextjs-config",
24
- "next.config.ts": "nextjs-config",
25
- "docker-compose.yml": "docker-compose",
26
- "docker-compose.yaml": "docker-compose",
27
- "fly.toml": "fly-config",
28
- "render.yaml": "render-config",
29
- "netlify.toml": "netlify-config",
30
- };
7
+ import { DEFAULT_EXCLUDES, EXTENSION_MAP, CONFIG_FILE_MAP } from "../utils/constants.js";
8
+ import { walkDirectory } from "../utils/walk-directory.js";
9
+ const require = createRequire(import.meta.url);
10
+ const pkg = require("../../package.json");
31
11
  // GuardVibe version — used in scan metadata
32
- const GUARDVIBE_VERSION = "1.4.0";
33
- function walkDirectory(dir, recursive, excludes, results) {
34
- let entries;
35
- try {
36
- entries = readdirSync(dir, { withFileTypes: true });
37
- }
38
- catch {
39
- return;
40
- }
41
- for (const entry of entries) {
42
- if (excludes.has(entry.name))
43
- continue;
44
- const fullPath = join(dir, entry.name);
45
- if (entry.isDirectory() && recursive) {
46
- walkDirectory(fullPath, recursive, excludes, results);
47
- }
48
- else if (entry.isFile()) {
49
- const ext = extname(entry.name).toLowerCase();
50
- if (EXTENSION_MAP[ext]) {
51
- results.push(fullPath);
52
- }
53
- if (entry.name.startsWith("Dockerfile") || entry.name.endsWith(".dockerfile")) {
54
- results.push(fullPath);
55
- }
56
- if (CONFIG_FILE_MAP[entry.name] && !results.includes(fullPath)) {
57
- results.push(fullPath);
58
- }
59
- }
60
- }
61
- }
12
+ const GUARDVIBE_VERSION = pkg.version;
62
13
  function hashContent(content) {
63
14
  return createHash("sha256").update(content).digest("hex").substring(0, 16);
64
15
  }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Shared constants used across GuardVibe tools.
3
+ * Single source of truth — all tool modules import from here.
4
+ */
5
+ /** Maps file extensions to language identifiers for security analysis. */
6
+ export declare const EXTENSION_MAP: Record<string, string>;
7
+ /** Maps well-known config filenames to their language/type identifier. */
8
+ export declare const CONFIG_FILE_MAP: Record<string, string>;
9
+ /** Directory names excluded from filesystem scans by default. */
10
+ export declare const DEFAULT_EXCLUDES: Set<string>;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Shared constants used across GuardVibe tools.
3
+ * Single source of truth — all tool modules import from here.
4
+ */
5
+ /** Maps file extensions to language identifiers for security analysis. */
6
+ export const EXTENSION_MAP = {
7
+ ".js": "javascript", ".jsx": "javascript", ".mjs": "javascript", ".cjs": "javascript",
8
+ ".ts": "typescript", ".tsx": "typescript", ".mts": "typescript", ".cts": "typescript",
9
+ ".py": "python", ".go": "go", ".html": "html",
10
+ ".sql": "sql", ".sh": "shell", ".bash": "shell",
11
+ ".yml": "yaml", ".yaml": "yaml",
12
+ ".tf": "terraform",
13
+ ".toml": "toml", ".json": "json",
14
+ };
15
+ /** Maps well-known config filenames to their language/type identifier. */
16
+ export const CONFIG_FILE_MAP = {
17
+ "vercel.json": "vercel-config",
18
+ "next.config.js": "nextjs-config",
19
+ "next.config.mjs": "nextjs-config",
20
+ "next.config.ts": "nextjs-config",
21
+ "docker-compose.yml": "docker-compose",
22
+ "docker-compose.yaml": "docker-compose",
23
+ "fly.toml": "fly-config",
24
+ "render.yaml": "render-config",
25
+ "netlify.toml": "netlify-config",
26
+ };
27
+ /** Directory names excluded from filesystem scans by default. */
28
+ export const DEFAULT_EXCLUDES = new Set([
29
+ "node_modules", ".git", "build", "dist", "vendor", "__pycache__",
30
+ ".next", ".nuxt", ".svelte-kit", "target", "bin", "obj",
31
+ "coverage", ".turbo", ".venv", "venv",
32
+ ]);
@@ -0,0 +1,3 @@
1
+ import type { SecurityRule } from "../data/rules/types.js";
2
+ export declare function setRules(rules: SecurityRule[]): void;
3
+ export declare function getRules(): SecurityRule[];
@@ -0,0 +1,7 @@
1
+ let registeredRules = [];
2
+ export function setRules(rules) {
3
+ registeredRules = rules;
4
+ }
5
+ export function getRules() {
6
+ return registeredRules;
7
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Shared recursive directory walker used by scan-directory, export-sarif,
3
+ * compliance-report, policy-check, and generate-policy tools.
4
+ */
5
+ /**
6
+ * Recursively walk a directory, collecting file paths that match
7
+ * known source extensions, Dockerfiles, or config file names.
8
+ *
9
+ * @param dir - Directory to walk
10
+ * @param recursive - Whether to descend into subdirectories
11
+ * @param excludes - Set of directory names to skip
12
+ * @param results - Accumulator array (mutated in place)
13
+ */
14
+ export declare function walkDirectory(dir: string, recursive: boolean, excludes: Set<string>, results: string[]): void;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Shared recursive directory walker used by scan-directory, export-sarif,
3
+ * compliance-report, policy-check, and generate-policy tools.
4
+ */
5
+ import { readdirSync } from "fs";
6
+ import { join, extname } from "path";
7
+ import { EXTENSION_MAP, CONFIG_FILE_MAP } from "./constants.js";
8
+ /**
9
+ * Recursively walk a directory, collecting file paths that match
10
+ * known source extensions, Dockerfiles, or config file names.
11
+ *
12
+ * @param dir - Directory to walk
13
+ * @param recursive - Whether to descend into subdirectories
14
+ * @param excludes - Set of directory names to skip
15
+ * @param results - Accumulator array (mutated in place)
16
+ */
17
+ export function walkDirectory(dir, recursive, excludes, results) {
18
+ let entries;
19
+ try {
20
+ entries = readdirSync(dir, { withFileTypes: true });
21
+ }
22
+ catch {
23
+ return;
24
+ }
25
+ for (const entry of entries) {
26
+ if (excludes.has(entry.name))
27
+ continue;
28
+ const fullPath = join(dir, entry.name);
29
+ if (entry.isDirectory() && recursive) {
30
+ walkDirectory(fullPath, recursive, excludes, results);
31
+ }
32
+ else if (entry.isFile()) {
33
+ const ext = extname(entry.name).toLowerCase();
34
+ if (EXTENSION_MAP[ext]) {
35
+ results.push(fullPath);
36
+ }
37
+ if (entry.name.startsWith("Dockerfile") || entry.name.endsWith(".dockerfile")) {
38
+ if (!results.includes(fullPath))
39
+ results.push(fullPath);
40
+ }
41
+ if (CONFIG_FILE_MAP[entry.name] && !results.includes(fullPath)) {
42
+ results.push(fullPath);
43
+ }
44
+ }
45
+ }
46
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "guardvibe",
3
- "version": "1.7.0",
4
- "description": "Security MCP for vibe coding. 267 rules, 14 tools for Next.js, Supabase, Clerk, Stripe, Prisma, tRPC, Hono, GraphQL, Convex, Turso, Uploadthing, AI SDK, and the full AI-generated stack.",
3
+ "version": "1.8.0",
4
+ "description": "Security MCP for vibe coding. 277 rules, 22 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/index.js",