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 +4 -4
- package/build/cli/audit.js +3 -0
- package/build/index.js +2 -2
- package/build/tools/check-code.js +54 -1
- package/build/tools/full-audit.js +17 -6
- package/build/tools/scan-secrets.js +3 -0
- package/build/utils/constants.d.ts +3 -0
- package/build/utils/constants.js +12 -0
- package/build/utils/walk-directory.js +3 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
[](https://www.npmjs.com/package/guardvibe)
|
|
7
7
|
[](https://codecov.io/gh/goklab/guardvibe)
|
|
8
8
|
|
|
9
|
-
**The security MCP built for vibe coding.**
|
|
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
|
-
- **
|
|
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 |
|
|
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 (
|
|
238
|
+
## Security Rules (365 rules across 25 modules)
|
|
239
239
|
|
|
240
240
|
| Category | Rules | Coverage |
|
|
241
241
|
|----------|-------|----------|
|
package/build/cli/audit.js
CHANGED
|
@@ -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.
|
|
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 (
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
235
|
+
allFindings.push({ ruleId: `DEP:${vuln2.id ?? "CVE"}`, severity: sev, file: "package.json", line: 0 });
|
|
229
236
|
}
|
|
230
237
|
}
|
|
231
|
-
|
|
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 :
|
|
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;
|
package/build/utils/constants.js
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "3.0.28",
|
|
4
4
|
"mcpName": "io.github.goklab/guardvibe",
|
|
5
|
-
"description": "Security MCP for vibe coding.
|
|
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",
|