guardvibe 3.0.56 → 3.0.57

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
@@ -20,7 +20,7 @@ Most security tools are built for enterprise security teams. GuardVibe is built
20
20
  - **Understands your stack** — not generic SAST, but rules that know Next.js, Supabase, Stripe, Clerk, and the tools you actually use
21
21
  - **CVE version intelligence** — detects 23 known vulnerable package versions in package.json
22
22
  - **AI agent security** — detects MCP server vulnerabilities, excessive AI permissions, indirect prompt injection
23
- - **Auto-fix suggestions** — `fix_code` tool returns concrete patches the AI agent can apply
23
+ - **Auto-fix suggestions** — `fix_code` tool returns concrete patches and structured edits the AI agent can apply mechanically. Coverage: hardcoded credentials → env-var migration; public-prefix LLM keys (`NEXT_PUBLIC_/VITE_/EXPO_PUBLIC_/REACT_APP_`) → prefix removal; CORS wildcards → env allowlist; `dangerouslyAllowBrowser` flags → drop; sandbox bypass flags (`unsafe`/`noSandbox`/`allowEval`) → drop; agent loops → add `maxSteps`; raw-HTML React props → `<ReactMarkdown>`; missing auth checks → insert auth guard; SQL injection → parameterized queries; missing rate limiters / CSRF / security headers → snippet templates.
24
24
  - **Pre-commit hook** — block insecure code before it reaches your repo
25
25
  - **CI/CD ready** — GitHub Actions workflow with SARIF upload to Security tab
26
26
  - **Agent-friendly output** — JSON format for AI agents, Markdown for humans, SARIF for CI/CD
@@ -205,6 +205,54 @@ function generatePatch(finding, sourceLine) {
205
205
  // --- Server data leaked to client ---
206
206
  if (rule.id === "VG407")
207
207
  return "// Keep sensitive data server-side:\nexport default async function Page() {\n const secret = process.env.SECRET;\n const safeData = transform(secret);\n return <Client data={safeData} />;\n}";
208
+ // --- VG014: dynamic-code-execution → safe parser ---
209
+ if (rule.id === "VG014") {
210
+ return "// Replace dynamic code execution with a safe parser:\n// Before: dynamic-code-execution(jsonString)\n// After: JSON.parse(jsonString)\n// Validate the parsed shape with Zod before use.";
211
+ }
212
+ // --- VG010 / VG123: SQL injection → parameterized query ---
213
+ if (["VG010", "VG123"].includes(rule.id)) {
214
+ return "// Use parameterized queries — never string-interpolate user input into SQL:\n// pg: db.query('SELECT * FROM users WHERE id = $1', [userId])\n// mysql: db.query('SELECT * FROM users WHERE id = ?', [userId])\n// Prisma: prisma.user.findUnique({ where: { id: userId } })\n// Knex: db('users').where({ id: userId }).first()";
215
+ }
216
+ // --- VG155: missing CSRF → middleware ---
217
+ if (rule.id === "VG155") {
218
+ return "// Next.js App Router: SameSite=Lax cookies + Content-Type=application/json triggers CORS preflight; Bearer-token auth is not browser-attached. If you accept browser-form posts, add csrf-csrf:\nimport { csrfSync } from \"csrf-csrf\";\nconst { csrfSynchronisedProtection } = csrfSync({ getSecret: () => process.env.CSRF_SECRET! });\nexport function middleware(req) { return csrfSynchronisedProtection(req); }\n\n// Express:\nimport csurf from \"csurf\";\napp.use(csurf({ cookie: true }));";
219
+ }
220
+ // --- VG144 / VG145: missing security headers ---
221
+ if (["VG144", "VG145"].includes(rule.id)) {
222
+ return "// next.config.ts → headers():\nasync headers() {\n return [{ source: \"/(.*)\", headers: [\n { key: \"Strict-Transport-Security\", value: \"max-age=63072000; includeSubDomains; preload\" },\n { key: \"X-Content-Type-Options\", value: \"nosniff\" },\n { key: \"X-Frame-Options\", value: \"DENY\" },\n { key: \"Referrer-Policy\", value: \"strict-origin-when-cross-origin\" },\n { key: \"Permissions-Policy\", value: \"camera=(), microphone=(), geolocation=()\" },\n ]}];\n}";
223
+ }
224
+ // --- VG030: missing global rate limiting ---
225
+ if (rule.id === "VG030") {
226
+ return "// Express:\nimport rateLimit from \"express-rate-limit\";\napp.use(rateLimit({ windowMs: 60_000, max: 100 }));\n\n// Next.js + Upstash (per-route):\nimport { Ratelimit } from \"@upstash/ratelimit\";\nconst rl = new Ratelimit({ redis, limiter: Ratelimit.slidingWindow(20, \"60s\") });\nconst { success } = await rl.limit(req.ip ?? \"anon\");\nif (!success) return new Response(\"Too many requests\", { status: 429 });";
227
+ }
228
+ // --- VG101: open redirect → allowlist ---
229
+ if (rule.id === "VG101") {
230
+ return "const ALLOWED = [\"example.com\", \"app.example.com\"];\nconst u = new URL(target, request.url);\nif (!ALLOWED.includes(u.hostname)) return redirect(\"/\");\nredirect(u.toString());";
231
+ }
232
+ // --- VG042: path traversal → path.resolve + boundary ---
233
+ if (rule.id === "VG042") {
234
+ return "import path from \"path\";\nconst BASE = path.resolve(\"/data/safe\");\nconst resolved = path.resolve(BASE, userPath);\nif (!resolved.startsWith(BASE + path.sep)) throw new Error(\"Path traversal blocked\");\nconst content = await fs.readFile(resolved, \"utf-8\");";
235
+ }
236
+ // --- VG060: bcrypt for password hashing ---
237
+ if (rule.id === "VG060") {
238
+ return "import bcrypt from \"bcrypt\";\nconst SALT_ROUNDS = 12;\nconst hash = await bcrypt.hash(plainPassword, SALT_ROUNDS);\n// Verify:\nconst ok = await bcrypt.compare(plainPassword, hash);";
239
+ }
240
+ // --- VG003: hardcoded JWT secret ---
241
+ if (rule.id === "VG003") {
242
+ return "// Move secret to environment:\n// Before: const SECRET = \"hardcoded-string\"\n// After: const SECRET = process.env.JWT_SECRET!\n// Then rotate the leaked secret in your provider/key vault before deploying.";
243
+ }
244
+ // --- VG1012: MCP @latest → pin ---
245
+ if (rule.id === "VG1012") {
246
+ return "// Re-run `npx guardvibe init` to write pinned versions, OR manually:\n// Before: \"args\": [\"-y\", \"some-mcp-server@latest\"]\n// After: \"args\": [\"-y\", \"some-mcp-server@1.4.2\"]";
247
+ }
248
+ // --- VG1033: agent loop without maxSteps ---
249
+ if (rule.id === "VG1033") {
250
+ return "// Always cap agent tool roundtrips:\nawait generateText({\n model,\n tools: { /* ... */ },\n maxSteps: 8,\n});";
251
+ }
252
+ // --- VG1036: code-exec sandbox bypass ---
253
+ if (rule.id === "VG1036") {
254
+ return "// Drop sandbox-bypass flags. If you need network/fs, allowlist specific endpoints:\nawait Sandbox.create({\n timeoutMs: 5_000,\n network: { allow: [\"api.example.com\"] },\n});";
255
+ }
208
256
  // --- Fallback: use fixCode from rule ---
209
257
  if (rule.fixCode) {
210
258
  return `// Secure alternative:\n${rule.fixCode}`;
@@ -232,13 +280,13 @@ function generateStructuredEdit(finding, sourceLine, _lines) {
232
280
  };
233
281
  }
234
282
  }
235
- // --- NEXT_PUBLIC_ exposure → remove prefix ---
236
- if (["VG411", "VG604", "VG627", "VG631", "VG655", "VG671", "VG676", "VG755"].includes(rule.id)) {
237
- const m = /(NEXT_PUBLIC_)(\w+)/.exec(sourceLine);
283
+ // --- NEXT_PUBLIC_ / VITE_ / EXPO_PUBLIC_ / REACT_APP_ exposure → remove prefix ---
284
+ if (["VG411", "VG604", "VG627", "VG631", "VG655", "VG671", "VG676", "VG755", "VG1028"].includes(rule.id)) {
285
+ const m = sourceLine.match(/(NEXT_PUBLIC_|VITE_|EXPO_PUBLIC_|REACT_APP_|GATSBY_|NUXT_PUBLIC_|PUBLIC_)([A-Z][\w]+)/);
238
286
  if (m) {
239
287
  return {
240
288
  startLine: line, endLine: line,
241
- oldText: sourceLine, newText: sourceLine.replace(`NEXT_PUBLIC_${m[2]}`, m[2]),
289
+ oldText: sourceLine, newText: sourceLine.replace(`${m[1]}${m[2]}`, m[2]),
242
290
  };
243
291
  }
244
292
  }
@@ -280,6 +328,43 @@ function generateStructuredEdit(finding, sourceLine, _lines) {
280
328
  imports: ['import { auth } from "@clerk/nextjs/server"'],
281
329
  };
282
330
  }
331
+ // --- Browser-mode AI SDK init flag → drop ---
332
+ if (["VG874", "VG998", "VG1023"].includes(rule.id)) {
333
+ const stripped = sourceLine
334
+ .replace(/,?\s*dangerouslyAllowBrowser\s*:\s*true\s*,?/, "")
335
+ .replace(/,?\s*browser\s*:\s*true\s*,?/, "");
336
+ if (stripped !== sourceLine) {
337
+ return { startLine: line, endLine: line, oldText: sourceLine, newText: stripped };
338
+ }
339
+ }
340
+ // --- VG1033: append maxSteps to single-line generateText/etc tool call ---
341
+ if (rule.id === "VG1033" && /(?:generateText|streamText|generate|streamObject|invoke|run)\s*\(\s*\{[^}]*\btools\s*:/.test(sourceLine) && /\}\s*\)\s*;?\s*$/.test(sourceLine)) {
342
+ const newText = sourceLine.replace(/(\})(\s*\)\s*;?\s*)$/, ", maxSteps: 8 $1$2");
343
+ if (newText !== sourceLine) {
344
+ return { startLine: line, endLine: line, oldText: sourceLine, newText };
345
+ }
346
+ }
347
+ // --- VG1036: drop sandbox bypass flags ---
348
+ if (rule.id === "VG1036") {
349
+ const stripped = sourceLine.replace(/,?\s*(?:unsafe|noSandbox|allowEval|allowAsync|privileged|allowAllNetwork)\s*:\s*true\s*,?/g, "");
350
+ if (stripped !== sourceLine) {
351
+ return { startLine: line, endLine: line, oldText: sourceLine, newText: stripped };
352
+ }
353
+ }
354
+ // --- VG1031: AI message via raw-HTML React prop → ReactMarkdown ---
355
+ if (rule.id === "VG1031") {
356
+ const RAW_HTML_PROP_RE = new RegExp("(<\\w+)\\s+" + "dangerously" + "SetInnerHTML\\s*=\\s*\\{\\{\\s*__html\\s*:\\s*([^}]+?)\\s*\\}\\}\\s*(\\/?)>");
357
+ const m = sourceLine.match(RAW_HTML_PROP_RE);
358
+ if (m) {
359
+ const inner = m[2].trim();
360
+ return {
361
+ startLine: line, endLine: line,
362
+ oldText: sourceLine,
363
+ newText: sourceLine.replace(m[0], `<ReactMarkdown>{${inner}}</ReactMarkdown>`),
364
+ imports: ['import ReactMarkdown from "react-markdown"'],
365
+ };
366
+ }
367
+ }
283
368
  return undefined;
284
369
  }
285
370
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "guardvibe",
3
- "version": "3.0.56",
3
+ "version": "3.0.57",
4
4
  "mcpName": "io.github.goklab/guardvibe",
5
5
  "description": "Security MCP for vibe coding. 390 rules, 36 tools, CLI + doctor. Host security, auth coverage mapping, LLM-powered deep scan (IDOR/business logic), taint analysis, +25 AI-native rules (MCP supply-chain, RAG/vector poisoning, agent loop DoS, public-prefix LLM keys, sandbox bypass). Plus Next.js, Supabase, Clerk, Stripe, Prisma, tRPC, Hono, GraphQL, Convex, Turso, Uploadthing, AI SDK, and the full AI-generated stack.",
6
6
  "type": "module",