guardvibe 3.1.1 → 3.1.3

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
@@ -64,7 +64,7 @@ GuardVibe is purpose-built for the AI coding workflow. Traditional tools are exc
64
64
  npx guardvibe init claude
65
65
  ```
66
66
 
67
- Creates `.claude.json` MCP config, `.claude/settings.json` auto-scan hooks, and `CLAUDE.md` security rules. Restart Claude Code after setup.
67
+ Creates `.mcp.json` MCP config (pinned to current version), `.claude/settings.json` auto-scan hooks, and `CLAUDE.md` security rules. Restart Claude Code after setup.
68
68
 
69
69
  ### Cursor
70
70
 
@@ -286,6 +286,13 @@ npx guardvibe doctor --scope host # + shell profiles, global MCP configs
286
286
  npx guardvibe doctor --scope full # + home dir configs
287
287
  npx guardvibe doctor --format json # JSON output
288
288
 
289
+ # LLM-powered deep scan (IDOR, business logic, race conditions, auth bypass)
290
+ npx guardvibe deep-scan <file> # Default: Haiku 4.5, all focus areas
291
+ npx guardvibe deep-scan <file> --focus idor # Narrow to IDOR
292
+ npx guardvibe deep-scan <file> --model sonnet # Deeper analysis (more expensive)
293
+ npx guardvibe deep-scan <file> --max-bytes 5000 # Truncate input for cost control
294
+ # Requires ANTHROPIC_API_KEY or OPENAI_API_KEY env var
295
+
289
296
  # Setup
290
297
  npx guardvibe init <platform> # Setup MCP server (claude, cursor, gemini, all)
291
298
  npx guardvibe hook install # Install pre-commit hook
@@ -443,9 +450,10 @@ If your AI agent cannot connect to GuardVibe:
443
450
 
444
451
  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.
445
452
  2. **Check the config path.** Run `npx guardvibe init claude` again and verify the output shows the correct config file location (`.mcp.json` in your project root for Claude Code, `.cursor/mcp.json` for Cursor).
446
- 3. **Re-run `init` to upgrade.** When upgrading GuardVibe, re-run `npx guardvibe init claude` — `.mcp.json` is pinned to a specific version (e.g. `guardvibe@3.0.41`) at init time for fast deterministic startup. Stale pins won't auto-update.
447
- 4. **Verify Node.js version.** GuardVibe requires Node.js >= 18.0.0. Check with `node --version`.
448
- 5. **Check npx cache.** If you upgraded GuardVibe and the old version is cached, run `npx -y guardvibe@latest` to force the latest version.
453
+ 3. **Re-run `init` to upgrade.** When upgrading GuardVibe, re-run `npx guardvibe init claude` — `.mcp.json` is pinned to a specific version (e.g. `guardvibe@3.1.3`) at init time for fast deterministic startup. As of v3.1.2 the re-run also rewrites stale pins automatically (`Upgraded GuardVibe pin (3.0.55 → 3.1.3)`). The same applies to `npx guardvibe hook install` and `npx guardvibe ci github` (since v3.1.3) — both are version-pinned at install/generate time and re-run to upgrade.
454
+ 4. **Pre-3.1.1 users won't see the auto-update banner.** GuardVibe started writing a once-per-day "newer version available" notice to stderr in v3.1.1. If your install predates that, you'll never see it — run `npx -y guardvibe@latest init <host>` once to bake in the latest pin and start receiving banners on subsequent sessions.
455
+ 5. **Verify Node.js version.** GuardVibe requires Node.js >= 18.0.0. Check with `node --version`.
456
+ 6. **Check npx cache.** If you upgraded GuardVibe and the old version is cached, run `npx -y guardvibe@latest` to force the latest version.
449
457
 
450
458
  ### Node.js version requirements
451
459
 
package/build/cli/ci.js CHANGED
@@ -2,9 +2,14 @@
2
2
  * CLI: guardvibe ci <provider>
3
3
  * Generates CI/CD workflow configurations.
4
4
  */
5
- import { writeFileSync, mkdirSync, existsSync } from "fs";
5
+ import { createRequire } from "module";
6
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
6
7
  import { join } from "path";
7
- const GITHUB_ACTIONS_WORKFLOW = `name: GuardVibe Security Scan
8
+ const require = createRequire(import.meta.url);
9
+ const pkg = require("../../package.json");
10
+ function buildGithubActionsWorkflow(version) {
11
+ return `name: GuardVibe Security Scan
12
+ # Pinned to guardvibe@${version} for reproducible CI builds. Re-run \`npx guardvibe ci github\` to upgrade.
8
13
 
9
14
  on:
10
15
  pull_request:
@@ -30,7 +35,7 @@ jobs:
30
35
  node-version: "22"
31
36
 
32
37
  - name: Run GuardVibe security scan
33
- run: npx -y guardvibe-scan --format sarif --output guardvibe-results.sarif
38
+ run: npx -y guardvibe@${version} scan --format sarif --output guardvibe-results.sarif
34
39
 
35
40
  - name: Upload SARIF to GitHub Security
36
41
  if: always()
@@ -39,18 +44,45 @@ jobs:
39
44
  sarif_file: guardvibe-results.sarif
40
45
  category: guardvibe
41
46
  `;
47
+ }
48
+ /** Extract a pinned guardvibe version from a generated workflow YAML, or "latest"/null for legacy/unrecognized forms. */
49
+ function extractPinnedVersionFromWorkflow(content) {
50
+ const pinned = content.match(/guardvibe@(\d+\.\d+\.\d+(?:-[\w.]+)?)/);
51
+ if (pinned)
52
+ return pinned[1];
53
+ if (/guardvibe-scan|guardvibe@latest/.test(content))
54
+ return "latest";
55
+ return null;
56
+ }
42
57
  function generateGitHubActions() {
43
58
  const workflowDir = join(process.cwd(), ".github", "workflows");
44
59
  if (!existsSync(workflowDir)) {
45
60
  mkdirSync(workflowDir, { recursive: true });
46
61
  }
47
62
  const workflowPath = join(workflowDir, "guardvibe.yml");
63
+ const fresh = buildGithubActionsWorkflow(pkg.version);
48
64
  if (existsSync(workflowPath)) {
49
- console.log(" [OK] .github/workflows/guardvibe.yml already exists.");
65
+ const existing = readFileSync(workflowPath, "utf-8");
66
+ const existingPin = extractPinnedVersionFromWorkflow(existing);
67
+ if (existingPin === pkg.version) {
68
+ console.log(` [OK] .github/workflows/guardvibe.yml already up-to-date (pinned to v${pkg.version}).`);
69
+ return;
70
+ }
71
+ if (existingPin && existingPin !== "latest") {
72
+ writeFileSync(workflowPath, fresh, "utf-8");
73
+ console.log(` [OK] Upgraded .github/workflows/guardvibe.yml (${existingPin} → ${pkg.version}).`);
74
+ return;
75
+ }
76
+ if (existingPin === "latest") {
77
+ writeFileSync(workflowPath, fresh, "utf-8");
78
+ console.log(` [OK] Pinned .github/workflows/guardvibe.yml (was unpinned → ${pkg.version}).`);
79
+ return;
80
+ }
81
+ console.log(" [OK] .github/workflows/guardvibe.yml exists with custom contents — leaving as-is.");
50
82
  return;
51
83
  }
52
- writeFileSync(workflowPath, GITHUB_ACTIONS_WORKFLOW, "utf-8");
53
- console.log(" [OK] Created .github/workflows/guardvibe.yml");
84
+ writeFileSync(workflowPath, fresh, "utf-8");
85
+ console.log(` [OK] Created .github/workflows/guardvibe.yml (pinned to v${pkg.version}).`);
54
86
  console.log(" [OK] SARIF results will appear in GitHub Security tab.");
55
87
  }
56
88
  export function runCi(args) {
package/build/cli/hook.js CHANGED
@@ -2,16 +2,23 @@
2
2
  * CLI: guardvibe hook install|uninstall
3
3
  * Manages pre-commit security hooks.
4
4
  */
5
+ import { createRequire } from "module";
5
6
  import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync, unlinkSync } from "fs";
6
7
  import { join } from "path";
7
- const HOOK_SCRIPT = `#!/bin/sh
8
- # GuardVibe pre-commit security hook
8
+ const require = createRequire(import.meta.url);
9
+ const pkg = require("../../package.json");
10
+ const GUARDVIBE_BLOCK_START = "# GuardVibe pre-commit security hook";
11
+ const GUARDVIBE_BLOCK_END = "✅ GuardVibe: all checks passed.";
12
+ function buildHookScript(version) {
13
+ return `#!/bin/sh
14
+ ${GUARDVIBE_BLOCK_START}
9
15
  # Installed by: npx guardvibe hook install
16
+ # Pinned to v${version} for reproducible CI/local behavior. Re-run install to upgrade.
10
17
 
11
18
  echo "🔒 GuardVibe: scanning staged files..."
12
19
 
13
20
  # Run guardvibe scan on staged files
14
- RESULT=$(npx -y guardvibe@latest scan --staged 2>&1)
21
+ RESULT=$(npx -y guardvibe@${version} scan --staged 2>&1)
15
22
  EXIT_CODE=$?
16
23
 
17
24
  if [ $EXIT_CODE -ne 0 ]; then
@@ -22,8 +29,33 @@ if [ $EXIT_CODE -ne 0 ]; then
22
29
  exit 1
23
30
  fi
24
31
 
25
- echo "✅ GuardVibe: all checks passed."
32
+ echo "${GUARDVIBE_BLOCK_END}"
26
33
  `;
34
+ }
35
+ /**
36
+ * Extract the pinned GuardVibe version from an existing pre-commit hook.
37
+ * Returns the version string, "latest" for legacy unpinned hooks, or null if no GuardVibe block found.
38
+ */
39
+ function extractPinnedVersionFromHook(content) {
40
+ const pinnedMatch = content.match(/guardvibe@(\d+\.\d+\.\d+(?:-[\w.]+)?)/);
41
+ if (pinnedMatch)
42
+ return pinnedMatch[1];
43
+ if (/guardvibe@latest|npx\s+-y\s+guardvibe(?:\s|\b)/.test(content) && content.includes("GuardVibe")) {
44
+ return "latest";
45
+ }
46
+ return null;
47
+ }
48
+ function replaceGuardVibeBlock(existing, fresh) {
49
+ // Strip any prior GuardVibe block (with or without leading shebang) and append the fresh one.
50
+ const cleaned = existing
51
+ .replace(/\n?# GuardVibe pre-commit security hook[\s\S]*?GuardVibe: all checks passed[."]*\n?/g, "")
52
+ .trimEnd();
53
+ if (!cleaned || cleaned === "#!/bin/sh")
54
+ return fresh;
55
+ // Splice the GuardVibe section in (without its shebang) onto the existing hook.
56
+ const freshNoShebang = fresh.replace(/^#!\/bin\/sh\n/, "");
57
+ return cleaned + "\n\n" + freshNoShebang;
58
+ }
27
59
  function installHook() {
28
60
  const gitDir = join(process.cwd(), ".git");
29
61
  if (!existsSync(gitDir)) {
@@ -35,19 +67,33 @@ function installHook() {
35
67
  mkdirSync(hooksDir, { recursive: true });
36
68
  }
37
69
  const hookPath = join(hooksDir, "pre-commit");
70
+ const freshScript = buildHookScript(pkg.version);
38
71
  if (existsSync(hookPath)) {
39
72
  const existing = readFileSync(hookPath, "utf-8");
40
- if (existing.includes("GuardVibe")) {
41
- console.log(" [OK] GuardVibe pre-commit hook already installed.");
73
+ const existingPin = extractPinnedVersionFromHook(existing);
74
+ if (existingPin === pkg.version) {
75
+ console.log(` [OK] GuardVibe pre-commit hook already up-to-date (v${pkg.version}).`);
76
+ return;
77
+ }
78
+ if (existingPin && existingPin !== "latest") {
79
+ writeFileSync(hookPath, replaceGuardVibeBlock(existing, freshScript), "utf-8");
80
+ chmodSync(hookPath, 0o755);
81
+ console.log(` [OK] Upgraded GuardVibe pre-commit hook (${existingPin} → ${pkg.version}).`);
82
+ return;
83
+ }
84
+ if (existingPin === "latest") {
85
+ writeFileSync(hookPath, replaceGuardVibeBlock(existing, freshScript), "utf-8");
86
+ chmodSync(hookPath, 0o755);
87
+ console.log(` [OK] Pinned GuardVibe pre-commit hook (was unpinned → ${pkg.version}).`);
42
88
  return;
43
89
  }
44
- writeFileSync(hookPath, existing + "\n" + HOOK_SCRIPT, "utf-8");
90
+ writeFileSync(hookPath, existing.trimEnd() + "\n\n" + freshScript, "utf-8");
45
91
  console.log(" [OK] GuardVibe added to existing pre-commit hook.");
46
92
  }
47
93
  else {
48
- writeFileSync(hookPath, HOOK_SCRIPT, "utf-8");
94
+ writeFileSync(hookPath, freshScript, "utf-8");
49
95
  chmodSync(hookPath, 0o755);
50
- console.log(" [OK] Pre-commit hook installed at .git/hooks/pre-commit");
96
+ console.log(` [OK] Pre-commit hook installed at .git/hooks/pre-commit (pinned to v${pkg.version}).`);
51
97
  }
52
98
  }
53
99
  function uninstallHook() {
package/build/cli/init.js CHANGED
@@ -14,6 +14,18 @@ const GUARDVIBE_MCP_CONFIG = {
14
14
  command: "npx",
15
15
  args: ["-y", `guardvibe@${pkg.version}`],
16
16
  };
17
+ /** Extract a pinned version from an existing MCP server config (`{ args: ["-y", "guardvibe@X.Y.Z"] }`). */
18
+ function extractPinnedVersion(config) {
19
+ const args = config?.args;
20
+ if (!Array.isArray(args))
21
+ return null;
22
+ for (const arg of args) {
23
+ if (typeof arg === "string" && arg.startsWith("guardvibe@")) {
24
+ return arg.slice("guardvibe@".length);
25
+ }
26
+ }
27
+ return null;
28
+ }
17
29
  const platforms = {
18
30
  claude: {
19
31
  path: join(process.cwd(), ".mcp.json"),
@@ -177,12 +189,27 @@ function setupPlatform(name) {
177
189
  if (!existing.mcpServers) {
178
190
  existing.mcpServers = {};
179
191
  }
180
- if (existing.mcpServers["guardvibe"]) {
181
- console.log(` [OK] GuardVibe already configured in ${platform.description}`);
192
+ const servers = existing.mcpServers;
193
+ if (servers["guardvibe"]) {
194
+ const existingPin = extractPinnedVersion(servers["guardvibe"]);
195
+ if (existingPin && existingPin !== pkg.version) {
196
+ servers["guardvibe"] = GUARDVIBE_MCP_CONFIG;
197
+ writeJsonFile(platform.path, existing);
198
+ console.log(` [OK] Upgraded GuardVibe pin in ${platform.description} (${existingPin} → ${pkg.version})`);
199
+ }
200
+ else if (!existingPin) {
201
+ // Existing config has no pin (legacy unpinned form) — overwrite to pin.
202
+ servers["guardvibe"] = GUARDVIBE_MCP_CONFIG;
203
+ writeJsonFile(platform.path, existing);
204
+ console.log(` [OK] Pinned GuardVibe in ${platform.description} (was unpinned → ${pkg.version})`);
205
+ }
206
+ else {
207
+ console.log(` [OK] GuardVibe already up-to-date in ${platform.description} (v${pkg.version})`);
208
+ }
182
209
  setupSecurityGuide(name);
183
210
  return true;
184
211
  }
185
- existing.mcpServers["guardvibe"] = GUARDVIBE_MCP_CONFIG;
212
+ servers["guardvibe"] = GUARDVIBE_MCP_CONFIG;
186
213
  writeJsonFile(platform.path, existing);
187
214
  }
188
215
  else {
@@ -15,8 +15,8 @@ export const complianceMetadata = {
15
15
  VG002: {
16
16
  gdpr: ["GDPR:Art32(1)(b)"],
17
17
  iso27001: ["ISO27001:A.8.3", "ISO27001:A.8.24"],
18
- exploit: "Attacker sends crafted SQL input through unvalidated form fields or URL parameters to extract, modify, or delete database records.",
19
- audit: "Demonstrate that all database queries use parameterized statements or ORM methods. Show code review checklist that includes SQL injection testing.",
18
+ exploit: "Attacker accesses API endpoints or resources without authentication, reading or modifying data belonging to other users.",
19
+ audit: "Show middleware/auth layer that protects all sensitive endpoints. Demonstrate that unauthenticated requests return 401/403.",
20
20
  },
21
21
  VG003: {
22
22
  gdpr: ["GDPR:Art32(1)(a)"],
@@ -27,8 +27,8 @@ export const complianceMetadata = {
27
27
  VG010: {
28
28
  gdpr: ["GDPR:Art32(1)(b)", "GDPR:Art25"],
29
29
  iso27001: ["ISO27001:A.8.3", "ISO27001:A.5.15"],
30
- exploit: "Attacker accesses API endpoints or resources without authentication, reading or modifying data belonging to other users.",
31
- audit: "Show middleware/auth layer that protects all sensitive endpoints. Demonstrate that unauthenticated requests return 401/403.",
30
+ exploit: "Attacker sends crafted SQL input (e.g., ' OR 1=1--, UNION SELECT, ;DROP TABLE) through unvalidated form fields, URL parameters, or JSON body to extract, modify, or delete database records.",
31
+ audit: "Demonstrate that all database queries use parameterized statements or ORM methods. Show code review checklist that includes SQL injection testing.",
32
32
  },
33
33
  VG042: {
34
34
  gdpr: ["GDPR:Art32(1)(a)"],
@@ -66,8 +66,10 @@ function getBreakingRisk(rule) {
66
66
  const id = rule.id;
67
67
  if (["VG001", "VG062", "VG060"].includes(id))
68
68
  return "LOW — Moving to env vars requires .env setup but no code logic changes.";
69
- if (["VG402", "VG010", "VG952"].includes(id))
69
+ if (["VG402", "VG952"].includes(id))
70
70
  return "MEDIUM — Adding auth checks may break unauthenticated flows that were working. Test all affected endpoints.";
71
+ if (["VG010", "VG011", "VG013", "VG014"].includes(id))
72
+ return "LOW — Parameterized queries are drop-in replacements for most drivers/ORMs. Manual string-concat call sites may need light refactoring; run query coverage after the swap.";
71
73
  if (["VG401", "VG960"].includes(id))
72
74
  return "MEDIUM — Adding schema validation will reject previously accepted invalid input. Test with real user data.";
73
75
  if (["VG403", "VG500", "VG510"].includes(id))
@@ -88,8 +90,10 @@ function getTestStrategy(rule) {
88
90
  const id = rule.id;
89
91
  if (["VG001", "VG062", "VG060"].includes(id))
90
92
  return "1. Move value to .env\n2. Verify app still reads from env\n3. Confirm old hardcoded value removed from git history";
91
- if (["VG402", "VG010"].includes(id))
93
+ if (["VG402"].includes(id))
92
94
  return "1. Call endpoint without auth token → expect 401\n2. Call with valid token → expect success\n3. Call with expired token → expect 401";
95
+ if (["VG010", "VG011", "VG013", "VG014"].includes(id))
96
+ return "1. Replace concatenated SQL with parameterized query / prepared statement\n2. Submit a malicious payload (`' OR 1=1--`, `;DROP TABLE--`, `UNION SELECT`) → expect literal match, no row exposure, no execution\n3. Re-run normal-input regression tests to confirm queries still return correct results";
93
97
  if (["VG401", "VG960"].includes(id))
94
98
  return "1. Submit valid data → expect success\n2. Submit empty/malformed data → expect 400 with validation error\n3. Submit oversized data → expect rejection";
95
99
  if (["VG403", "VG500"].includes(id))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "guardvibe",
3
- "version": "3.1.1",
3
+ "version": "3.1.3",
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",