guardvibe 3.0.26 → 3.0.28

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
@@ -6,7 +6,7 @@
6
6
  [![npm provenance](https://img.shields.io/badge/provenance-verified-brightgreen)](https://www.npmjs.com/package/guardvibe)
7
7
  [![codecov](https://codecov.io/gh/goklab/guardvibe/graph/badge.svg)](https://codecov.io/gh/goklab/guardvibe)
8
8
 
9
- **The security MCP built for vibe coding.** 335 security rules, 36 tools covering the entire AI-generated code journey — from first line to production deployment.
9
+ **The security MCP built for vibe coding.** 365 security rules, 36 tools covering the entire AI-generated code journey — from first line to production deployment.
10
10
 
11
11
  Works with **Claude Code, Cursor, Gemini CLI, Codex, VS Code (Copilot), Windsurf**, and any MCP-compatible coding agent.
12
12
 
@@ -14,7 +14,7 @@ Works with **Claude Code, Cursor, Gemini CLI, Codex, VS Code (Copilot), Windsurf
14
14
 
15
15
  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.
16
16
 
17
- - **335 security rules, 36 tools** purpose-built for the stacks AI agents generate
17
+ - **365 security rules, 36 tools** purpose-built for the stacks AI agents generate
18
18
  - **Zero setup friction** — `npx guardvibe` and you're scanning
19
19
  - **No account required** — runs 100% locally, no API keys, no cloud
20
20
  - **Understands your stack** — not generic SAST, but rules that know Next.js, Supabase, Stripe, Clerk, and the tools you actually use
@@ -50,7 +50,7 @@ GuardVibe is purpose-built for the AI coding workflow. Traditional tools are exc
50
50
  | CVE version detection | 23 packages | Extensive | Extensive |
51
51
  | Compliance mapping (SOC2, PCI-DSS, HIPAA) | Built-in | Paid tier | None |
52
52
  | SARIF CI/CD export | Yes | Yes | Limited |
53
- | Rule count | 335 (focused) | 5000+ (broad) | N/A |
53
+ | Rule count | 365 (focused) | 5000+ (broad) | N/A |
54
54
 
55
55
  **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.
56
56
 
@@ -235,7 +235,7 @@ Malicious postinstall scripts, unpinned GitHub Actions, typosquat detection
235
235
 
236
236
  All scanning tools support `format: "json"` for machine-readable output.
237
237
 
238
- ## Security Rules (341 rules across 25 modules)
238
+ ## Security Rules (365 rules across 25 modules)
239
239
 
240
240
  | Category | Rules | Coverage |
241
241
  |----------|-------|----------|
@@ -6,6 +6,8 @@ import { writeFileSync, existsSync, mkdirSync } from "fs";
6
6
  import { resolve, dirname } from "path";
7
7
  import { parseArgs, getStringFlag, getOutputPath, shouldFail, validateFormat } from "./args.js";
8
8
  import { runFullAudit, formatAuditResult } from "../tools/full-audit.js";
9
+ import { setRules } from "../utils/rule-registry.js";
10
+ import { builtinRules } from "../data/rules/index.js";
9
11
  export async function runAudit(args) {
10
12
  const { flags, positional } = parseArgs(args);
11
13
  const targetPath = resolve(positional[0] ?? ".");
@@ -14,6 +16,7 @@ export async function runAudit(args) {
14
16
  const failOn = getStringFlag(flags, "fail-on") ?? "critical";
15
17
  const skipDeps = flags["skip-deps"] === true;
16
18
  const skipSecrets = flags["skip-secrets"] === true;
19
+ setRules(builtinRules);
17
20
  // Terminal format by default when outputting to TTY, unless --format is specified
18
21
  const isTerminal = !outputFile && process.stdout.isTTY && !flags["format"];
19
22
  const format = isTerminal ? "terminal" : rawFormat;
package/build/index.js CHANGED
@@ -60,7 +60,7 @@ function mergeStatsIntoOutput(results, summary, format) {
60
60
  const server = new McpServer({
61
61
  name: "guardvibe",
62
62
  version: pkg.version,
63
- description: "Security MCP for vibe coding — single source of truth for AI assistants. 335 security rules and 36 tools. Use full_audit for a comprehensive PASS/FAIL/WARN verdict with deterministic result hash, coverage %, and unified report across code, secrets, dependencies, config, taint analysis, and auth coverage. IMPORTANT: When full_audit returns FAIL/WARN, call remediation_plan to get a mandatory section-by-section fix checklist covering ALL 6 sections (not just code). After fixing, call verify_remediation to confirm all sections were addressed. Same code = same hash = same results regardless of which AI assistant runs it. Covers OWASP, Next.js, Supabase, Stripe, Clerk, Prisma, Hono, AI SDK, MCP server security, host hardening. Maps to SOC2, PCI-DSS, HIPAA, GDPR, ISO27001, EU AI Act. Runs 100% locally with zero configuration.",
63
+ description: "Security MCP for vibe coding — single source of truth for AI assistants. 365 security rules and 36 tools. Use full_audit for a comprehensive PASS/FAIL/WARN verdict with deterministic result hash, coverage %, and unified report across code, secrets, dependencies, config, taint analysis, and auth coverage. IMPORTANT: When full_audit returns FAIL/WARN, call remediation_plan to get a mandatory section-by-section fix checklist covering ALL 6 sections (not just code). After fixing, call verify_remediation to confirm all sections were addressed. Same code = same hash = same results regardless of which AI assistant runs it. Covers OWASP, Next.js, Supabase, Stripe, Clerk, Prisma, Hono, AI SDK, MCP server security, host hardening. Maps to SOC2, PCI-DSS, HIPAA, GDPR, ISO27001, EU AI Act. Runs 100% locally with zero configuration.",
64
64
  });
65
65
  // Tool 1: Analyze code for security vulnerabilities
66
66
  server.tool("check_code", "Analyze inline code for security vulnerabilities (OWASP Top 10, XSS, SQL injection, insecure patterns). Pass code as a string parameter. For scanning files on disk, use scan_file instead. Example: check_code({code: 'app.get(...)', language: 'javascript'})", {
@@ -884,7 +884,7 @@ server.tool("deep_scan", "LLM-powered deep security analysis for vulnerabilities
884
884
  return { content: [{ type: "text", text: output }] };
885
885
  });
886
886
  // Tool 33: Full audit — single source of truth
887
- server.tool("full_audit", "Single command that runs ALL checks: code scan (341 rules), secret detection, dependency CVEs, config audit, taint analysis, and auth coverage. Returns PASS/FAIL/WARN verdict with deterministic hash. IMPORTANT: If verdict is FAIL or WARN, you MUST call remediation_plan next to get a section-by-section fix checklist — do NOT skip any section. After fixing, call verify_remediation to confirm ALL sections are addressed. Example: full_audit({path: '.'})", {
887
+ server.tool("full_audit", "Single command that runs ALL checks: code scan (365 rules), secret detection, dependency CVEs, config audit, taint analysis, and auth coverage. Returns PASS/FAIL/WARN verdict with deterministic hash. IMPORTANT: If verdict is FAIL or WARN, you MUST call remediation_plan next to get a section-by-section fix checklist — do NOT skip any section. After fixing, call verify_remediation to confirm ALL sections are addressed. Example: full_audit({path: '.'})", {
888
888
  path: z.string().default(".").describe("Project root directory"),
889
889
  format: z.enum(["markdown", "json"]).default("markdown").describe("Output format"),
890
890
  skipDeps: z.boolean().default(false).describe("Skip dependency vulnerability check"),
@@ -304,6 +304,20 @@ export function analyzeCode(code, language, framework, filePath, configDir, rule
304
304
  continue;
305
305
  if (rule.id.startsWith("VG21") && !filePath && language !== "yaml")
306
306
  continue;
307
+ // Skip credential rules in test files — fixtures and assertions intentionally use fake values.
308
+ const isTestFile = filePath && /(?:\.(?:spec|test|e2e|stories)\.(?:ts|tsx|js|jsx|mjs|cjs)$|\/__tests__\/|\/tests?\/|\/cypress\/|\/playwright\/)/i.test(filePath);
309
+ if (isTestFile && (rule.id === "VG001" || rule.id === "VG062"))
310
+ continue;
311
+ // Skip Expo-specific rule (VG708) when project is not an Expo app.
312
+ // The rule's regex incorrectly matches the literal strings "app.json"/"app.config.ts"
313
+ // appearing in unrelated configs (e.g. angular.json's tsConfig field).
314
+ if (rule.id === "VG708" && filePath) {
315
+ const fileName = filePath.split("/").pop() ?? "";
316
+ const isExpoConfigFile = /^app\.(json|config\.(js|ts|mjs|cjs))$/.test(fileName);
317
+ const importsExpo = /(?:from\s+['"]expo[\w-]*['"]|require\s*\(\s*['"]expo[\w-]*['"])/i.test(code);
318
+ if (!isExpoConfigFile && !importsExpo)
319
+ continue;
320
+ }
307
321
  // ── Context-aware rule skipping (pattern-agnostic) ──────────────
308
322
  const authRuleIds = new Set(["VG420", "VG952", "VG002", "VG402"]);
309
323
  const adminRoleRuleIds = new Set(["VG426", "VG957"]);
@@ -538,6 +552,28 @@ export function analyzeCode(code, language, framework, filePath, configDir, rule
538
552
  if (isHumanReadableString(lines, lineNumber))
539
553
  continue;
540
554
  }
555
+ // Skip credential rules when the variable name signals test/example/mock intent.
556
+ // e.g. `testingPassword`, `examplePassword`, `mockApiKey`, `placeholderSecret`.
557
+ if (rule.id === "VG001" || rule.id === "VG062") {
558
+ const matchedLine = lines[lineNumber - 1] ?? "";
559
+ if (/(?:^|\s|\b)(?:testing|example|mock|placeholder|sample|demo|fake|dummy|stub|fixture)[A-Z_]/.test(matchedLine))
560
+ continue;
561
+ }
562
+ // Skip VG010 (SQL injection) on Angular HTTP service calls — http.get/post/etc.
563
+ // are HTTP client methods, not SQL. The existing pattern's `get` keyword catches them.
564
+ if (rule.id === "VG010") {
565
+ const matchedLine = lines[lineNumber - 1] ?? "";
566
+ const isHttpClientCall = /(?:this\.)?(?:http|httpClient|httpService|api|client)\.(?:get|post|put|delete|patch|head|options)\s*\(/i.test(matchedLine);
567
+ const importsHttpClient = /from\s+['"]@angular\/common\/http['"]/i.test(code) || /import\s+.*HttpClient/i.test(code);
568
+ const fileIsAngularService = filePath ? /\.(?:service|component|directive|pipe|guard|resolver|interceptor)\.ts$/i.test(filePath) : false;
569
+ if (isHttpClientCall && (importsHttpClient || fileIsAngularService))
570
+ continue;
571
+ // Also skip when matched call has no SQL keyword anywhere on the line — covers fetch/axios template-literal URLs.
572
+ const hasSqlKeyword = /\b(?:SELECT|INSERT|UPDATE|DELETE|FROM|WHERE|JOIN|UNION|DROP|TRUNCATE|ALTER|CREATE\s+TABLE)\b/i.test(matchedLine);
573
+ const isFetchOrAxios = /(?:fetch|axios|got|ky|undici|request)\s*[.\(]|axios\.(?:get|post|put|delete|patch)/i.test(matchedLine);
574
+ if (isFetchOrAxios && !hasSqlKeyword)
575
+ continue;
576
+ }
541
577
  // Skip supply chain rules for known legitimate packages
542
578
  if (["VG872", "VG873"].includes(rule.id)) {
543
579
  const pkgMatch = /"([\w@/-]+)"/.exec(match[0]);
@@ -594,9 +630,21 @@ export function analyzeCode(code, language, framework, filePath, configDir, rule
594
630
  * Prefers framework-specific rules (VG4xx, VG9xx) over generic core rules (VG0xx).
595
631
  */
596
632
  function deduplicateFindings(findings) {
633
+ // First pass: drop exact duplicates — same rule firing on the same line.
634
+ // Happens when a rule has multiple regex variants that all match the same position,
635
+ // typical on minified single-line files where many patterns hit line 2 or 3.
636
+ const seenExact = new Set();
637
+ const exactDeduped = [];
638
+ for (const f of findings) {
639
+ const key = `${f.rule.id}:${f.line}`;
640
+ if (seenExact.has(key))
641
+ continue;
642
+ seenExact.add(key);
643
+ exactDeduped.push(f);
644
+ }
597
645
  // Group findings by line number
598
646
  const byLine = new Map();
599
- for (const f of findings) {
647
+ for (const f of exactDeduped) {
600
648
  const group = byLine.get(f.line);
601
649
  if (group)
602
650
  group.push(f);
@@ -637,6 +685,11 @@ function isDuplicatePair(a, b) {
637
685
  // Same rule name = same vulnerability
638
686
  if (a.rule.name === b.rule.name)
639
687
  return true;
688
+ // Both are SQL injection variants — VG010 (generic) and VG123 (template literal specific) overlap.
689
+ // VG123 is more specific so it should dominate. isMoreSpecific handles the prefix order.
690
+ const sqlInjectionRules = new Set(["VG010", "VG123"]);
691
+ if (sqlInjectionRules.has(a.rule.id) && sqlInjectionRules.has(b.rule.id))
692
+ return true;
640
693
  // Both are XSS/innerHTML related — the core VG012+VG408 duplicate case
641
694
  if (a.rule.name.includes("innerHTML") && b.rule.name.includes("innerHTML"))
642
695
  return true;
@@ -210,25 +210,36 @@ export async function runFullAudit(path, options) {
210
210
  const depsJson = await scanDependencies(manifestPath, "json");
211
211
  const parsed = safeJsonParse(depsJson);
212
212
  if (parsed) {
213
- const vuln = parsed.summary?.vulnerable ?? 0;
214
- const counts = { findings: vuln, critical: parsed.summary?.critical ?? 0, high: parsed.summary?.high ?? 0, medium: parsed.summary?.medium ?? 0 };
213
+ const vulnPackages = parsed.summary?.vulnerable ?? 0;
215
214
  const depFindings = [];
215
+ let depCritical = 0, depHigh = 0, depMedium = 0;
216
216
  for (const pkg of parsed.packages ?? []) {
217
217
  for (const v of pkg.vulnerabilities ?? []) {
218
218
  const vuln2 = v;
219
+ const sev = (vuln2.severity ?? "high");
220
+ if (sev === "critical")
221
+ depCritical++;
222
+ else if (sev === "high")
223
+ depHigh++;
224
+ else
225
+ depMedium++;
219
226
  depFindings.push({
220
227
  ruleId: `DEP:${(vuln2.id ?? "CVE")}`,
221
- severity: (vuln2.severity ?? "high"),
228
+ severity: sev,
222
229
  file: "package.json",
223
230
  line: 0,
224
231
  name: `${pkg.name ?? "unknown"}: ${(vuln2.id ?? "CVE")}`,
225
232
  description: (vuln2.summary ?? vuln2.details ?? ""),
226
233
  fix: `Run: npm update ${pkg.name ?? ""}`,
227
234
  });
228
- allFindings.push({ ruleId: `DEP:${vuln2.id ?? "CVE"}`, severity: vuln2.severity, file: "package.json", line: 0 });
235
+ allFindings.push({ ruleId: `DEP:${vuln2.id ?? "CVE"}`, severity: sev, file: "package.json", line: 0 });
229
236
  }
230
237
  }
231
- sections.push({ name: "dependencies", status: "ok", ...counts, details: vuln === 0 ? "No known CVEs" : `${vuln} vulnerable package(s)`, sectionFindings: depFindings });
238
+ const counts = { findings: depFindings.length, critical: depCritical, high: depHigh, medium: depMedium };
239
+ const detailText = depFindings.length === 0
240
+ ? "No known CVEs"
241
+ : `${depFindings.length} CVE(s) across ${vulnPackages} vulnerable package(s)`;
242
+ sections.push({ name: "dependencies", status: "ok", ...counts, details: detailText, sectionFindings: depFindings });
232
243
  }
233
244
  }
234
245
  catch {
@@ -339,7 +350,7 @@ export async function runFullAudit(path, options) {
339
350
  const totalHigh = sections.reduce((s, sec) => s + sec.high, 0);
340
351
  const totalMedium = sections.reduce((s, sec) => s + sec.medium, 0);
341
352
  const totalFindings = sections.reduce((s, sec) => s + sec.findings, 0);
342
- const rulesApplied = rules.length > 0 ? rules.length : 335;
353
+ const rulesApplied = rules.length > 0 ? rules.length : 365;
343
354
  // Adjust score to reflect ALL sections, not just code
344
355
  // Each critical finding deducts 5 points, high deducts 3, medium deducts 1
345
356
  // Score from code scan is the baseline, other sections reduce it further
@@ -3,6 +3,7 @@ import { basename, dirname, extname, join, relative, resolve } from "path";
3
3
  import { execFileSync } from "child_process";
4
4
  import { secretPatterns, calculateEntropy } from "../data/secret-patterns.js";
5
5
  import { loadConfig } from "../utils/config.js";
6
+ import { isExcludedFilename } from "../utils/constants.js";
6
7
  const DEFAULT_SECRET_EXCLUDES = new Set(["node_modules", ".git", "build", "dist"]);
7
8
  const SOURCE_FILE_EXTENSIONS = new Set([
8
9
  ".js", ".jsx", ".mjs", ".cjs",
@@ -75,6 +76,8 @@ function walkForSecrets(dir, recursive, results, excludes) {
75
76
  if (!entry.isFile())
76
77
  continue;
77
78
  const name = entry.name;
79
+ if (isExcludedFilename(name))
80
+ continue;
78
81
  const ext = extname(name).toLowerCase();
79
82
  if (name.startsWith(".env") || CONFIG_FILE_EXTENSIONS.has(ext) || SOURCE_FILE_EXTENSIONS.has(ext)) {
80
83
  results.push(fullPath);
@@ -8,3 +8,6 @@ export declare const EXTENSION_MAP: Record<string, string>;
8
8
  export declare const CONFIG_FILE_MAP: Record<string, string>;
9
9
  /** Directory names excluded from filesystem scans by default. */
10
10
  export declare const DEFAULT_EXCLUDES: Set<string>;
11
+ /** File-name patterns excluded from scans — minified bundles, vendor libs, generated artifacts. */
12
+ export declare const DEFAULT_FILE_PATTERN_EXCLUDES: RegExp[];
13
+ export declare function isExcludedFilename(name: string): boolean;
@@ -32,3 +32,15 @@ export const DEFAULT_EXCLUDES = new Set([
32
32
  ".vercel", ".clerk", ".wrangler", ".netlify", ".amplify",
33
33
  ".serverless", ".firebase", ".expo", ".output",
34
34
  ]);
35
+ /** File-name patterns excluded from scans — minified bundles, vendor libs, generated artifacts. */
36
+ export const DEFAULT_FILE_PATTERN_EXCLUDES = [
37
+ /\.min\.(js|mjs|cjs|css)$/i,
38
+ /\.bundle\.(js|mjs|cjs)$/i,
39
+ /-bundle\.(js|mjs|cjs)$/i,
40
+ /\.production(\.min)?\.(js|mjs|cjs)$/i,
41
+ /\.umd(\.min)?\.(js|mjs|cjs)$/i,
42
+ /\.esm(\.min)?\.(js|mjs|cjs)$/i,
43
+ ];
44
+ export function isExcludedFilename(name) {
45
+ return DEFAULT_FILE_PATTERN_EXCLUDES.some((pattern) => pattern.test(name));
46
+ }
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import { readdirSync } from "fs";
6
6
  import { join, extname } from "path";
7
- import { EXTENSION_MAP, CONFIG_FILE_MAP } from "./constants.js";
7
+ import { EXTENSION_MAP, CONFIG_FILE_MAP, isExcludedFilename } from "./constants.js";
8
8
  /**
9
9
  * Recursively walk a directory, collecting file paths that match
10
10
  * known source extensions, Dockerfiles, or config file names.
@@ -31,6 +31,8 @@ export function walkDirectory(dir, recursive, excludes, results, unsupportedResu
31
31
  walkDirectory(fullPath, recursive, excludes, results, unsupportedResults);
32
32
  }
33
33
  else if (entry.isFile()) {
34
+ if (isExcludedFilename(entry.name))
35
+ continue;
34
36
  const ext = extname(entry.name).toLowerCase();
35
37
  let matched = false;
36
38
  if (EXTENSION_MAP[ext]) {
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "guardvibe",
3
- "version": "3.0.26",
3
+ "version": "3.0.28",
4
4
  "mcpName": "io.github.goklab/guardvibe",
5
- "description": "Security MCP for vibe coding. 341 rules, 36 tools, CLI + doctor. Host security, auth coverage mapping, LLM-powered deep scan (IDOR/business logic), taint analysis. Plus Next.js, Supabase, Clerk, Stripe, Prisma, tRPC, Hono, GraphQL, Convex, Turso, Uploadthing, AI SDK, and the full AI-generated stack.",
5
+ "description": "Security MCP for vibe coding. 365 rules, 36 tools, CLI + doctor. Host security, auth coverage mapping, LLM-powered deep scan (IDOR/business logic), taint analysis. 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",
7
7
  "bin": {
8
8
  "guardvibe": "build/cli.js",