guardvibe 3.0.3 → 3.0.6
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/build/cli/check-cmd.d.ts +1 -0
- package/build/cli/check-cmd.js +17 -0
- package/build/cli/explain.d.ts +1 -0
- package/build/cli/explain.js +17 -0
- package/build/cli/fix.d.ts +1 -0
- package/build/cli/fix.js +35 -0
- package/build/cli.js +15 -0
- package/build/index.js +143 -28
- package/build/tools/auth-coverage.js +4 -2
- package/build/tools/compliance-report.js +27 -2
- package/build/tools/scan-dependencies.js +7 -1
- package/build/tools/scan-directory.js +10 -0
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runCheckCmd(args: string[]): Promise<void>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI: guardvibe check-cmd "<command>"
|
|
3
|
+
* Check if a shell command is safe to execute.
|
|
4
|
+
*/
|
|
5
|
+
import { parseArgs } from "./args.js";
|
|
6
|
+
import { checkCommand } from "../tools/check-command.js";
|
|
7
|
+
export async function runCheckCmd(args) {
|
|
8
|
+
const { flags, positional } = parseArgs(args);
|
|
9
|
+
const command = positional.join(" ");
|
|
10
|
+
if (!command) {
|
|
11
|
+
console.error(' [ERR] Please specify a command: npx guardvibe check-cmd "rm -rf /"');
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
const format = (flags.format === "json" ? "json" : "markdown");
|
|
15
|
+
const result = checkCommand(command, process.cwd(), undefined, format);
|
|
16
|
+
console.log(result);
|
|
17
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runExplain(args: string[]): Promise<void>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI: guardvibe explain <ruleId>
|
|
3
|
+
* Get detailed remediation guidance for a security rule.
|
|
4
|
+
*/
|
|
5
|
+
import { parseArgs } from "./args.js";
|
|
6
|
+
import { explainRemediation } from "../tools/explain-remediation.js";
|
|
7
|
+
export async function runExplain(args) {
|
|
8
|
+
const { flags, positional } = parseArgs(args);
|
|
9
|
+
const ruleId = positional[0];
|
|
10
|
+
if (!ruleId) {
|
|
11
|
+
console.error(" [ERR] Please specify a rule ID: npx guardvibe explain VG154");
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
const format = (flags.format === "json" ? "json" : "markdown");
|
|
15
|
+
const result = explainRemediation(ruleId, undefined, format);
|
|
16
|
+
console.log(result);
|
|
17
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runFix(args: string[]): Promise<void>;
|
package/build/cli/fix.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI: guardvibe fix <file>
|
|
3
|
+
* Get security fix suggestions for a file.
|
|
4
|
+
*/
|
|
5
|
+
import { readFileSync } from "fs";
|
|
6
|
+
import { resolve, extname } from "path";
|
|
7
|
+
import { parseArgs } from "./args.js";
|
|
8
|
+
import { fixCode } from "../tools/fix-code.js";
|
|
9
|
+
import { EXTENSION_MAP } from "../utils/constants.js";
|
|
10
|
+
export async function runFix(args) {
|
|
11
|
+
const { flags, positional } = parseArgs(args);
|
|
12
|
+
const filePath = positional[0];
|
|
13
|
+
if (!filePath) {
|
|
14
|
+
console.error(" [ERR] Please specify a file: npx guardvibe fix src/app/api/route.ts");
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
const resolved = resolve(filePath);
|
|
18
|
+
let content;
|
|
19
|
+
try {
|
|
20
|
+
content = readFileSync(resolved, "utf-8");
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
console.error(` [ERR] Could not read file: ${resolved}`);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
const ext = extname(resolved).toLowerCase();
|
|
27
|
+
const language = EXTENSION_MAP[ext];
|
|
28
|
+
if (!language) {
|
|
29
|
+
console.error(` [ERR] Unsupported file type: ${ext}`);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
const format = (flags.format === "json" ? "json" : "markdown");
|
|
33
|
+
const result = fixCode(content, language, undefined, resolved, format);
|
|
34
|
+
console.log(result);
|
|
35
|
+
}
|
package/build/cli.js
CHANGED
|
@@ -23,6 +23,9 @@ function printUsage() {
|
|
|
23
23
|
npx guardvibe check <file> Scan a single file for security issues
|
|
24
24
|
npx guardvibe doctor [path] Run host security audit
|
|
25
25
|
npx guardvibe audit [path] Full security audit with PASS/FAIL verdict
|
|
26
|
+
npx guardvibe explain <ruleId> Get detailed remediation guidance for a rule
|
|
27
|
+
npx guardvibe fix <file> Get security fix suggestions for a file
|
|
28
|
+
npx guardvibe check-cmd "<cmd>" Check if a shell command is safe to execute
|
|
26
29
|
npx guardvibe init <platform> Setup MCP server configuration
|
|
27
30
|
npx guardvibe hook install Install pre-commit security hook
|
|
28
31
|
npx guardvibe hook uninstall Remove pre-commit security hook
|
|
@@ -127,6 +130,18 @@ async function main() {
|
|
|
127
130
|
const { runAudit } = await import("./cli/audit.js");
|
|
128
131
|
await runAudit(subArgs);
|
|
129
132
|
}
|
|
133
|
+
else if (command === "explain") {
|
|
134
|
+
const { runExplain } = await import("./cli/explain.js");
|
|
135
|
+
await runExplain(subArgs);
|
|
136
|
+
}
|
|
137
|
+
else if (command === "fix") {
|
|
138
|
+
const { runFix } = await import("./cli/fix.js");
|
|
139
|
+
await runFix(subArgs);
|
|
140
|
+
}
|
|
141
|
+
else if (command === "check-cmd") {
|
|
142
|
+
const { runCheckCmd } = await import("./cli/check-cmd.js");
|
|
143
|
+
await runCheckCmd(subArgs);
|
|
144
|
+
}
|
|
130
145
|
else {
|
|
131
146
|
console.error(` Unknown command: ${command}`);
|
|
132
147
|
printUsage();
|
package/build/index.js
CHANGED
|
@@ -43,13 +43,25 @@ import { fixCode as fixCodeTool } from "./tools/fix-code.js";
|
|
|
43
43
|
import { analyzeAuthCoverage, formatAuthCoverage } from "./tools/auth-coverage.js";
|
|
44
44
|
import { buildDeepScanPrompt, parseDeepScanResult, formatDeepScanFindings, callLLM } from "./tools/deep-scan.js";
|
|
45
45
|
import { runFullAudit, formatAuditResult } from "./tools/full-audit.js";
|
|
46
|
+
// Helper: merge stats summary into JSON output instead of concatenating two JSON objects
|
|
47
|
+
function mergeStatsIntoOutput(results, summary, format) {
|
|
48
|
+
if (format === "json" && summary) {
|
|
49
|
+
try {
|
|
50
|
+
const parsed = JSON.parse(results);
|
|
51
|
+
const stats = JSON.parse(summary);
|
|
52
|
+
return JSON.stringify({ ...parsed, _meta: stats.guardvibeStats ?? stats });
|
|
53
|
+
}
|
|
54
|
+
catch { /* fall through */ }
|
|
55
|
+
}
|
|
56
|
+
return results + summary;
|
|
57
|
+
}
|
|
46
58
|
const server = new McpServer({
|
|
47
59
|
name: "guardvibe",
|
|
48
60
|
version: pkg.version,
|
|
49
61
|
description: "Security MCP for vibe coding — single source of truth for AI assistants. 334 security rules and 34 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. 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.",
|
|
50
62
|
});
|
|
51
63
|
// Tool 1: Analyze code for security vulnerabilities
|
|
52
|
-
server.tool("check_code", "Analyze code for security vulnerabilities (OWASP Top 10, XSS, SQL injection, insecure patterns).
|
|
64
|
+
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'})", {
|
|
53
65
|
code: z.string().describe("The code snippet to analyze"),
|
|
54
66
|
language: z
|
|
55
67
|
.enum(["javascript", "typescript", "python", "go", "dockerfile", "html", "sql", "shell", "yaml", "terraform", "firestore"])
|
|
@@ -67,7 +79,7 @@ server.tool("check_code", "Analyze code for security vulnerabilities (OWASP Top
|
|
|
67
79
|
recordScan(cwd, { toolName: "check_code", filesScanned: 1, findings: findings.map(f => ({ severity: f.rule.severity, ruleId: f.rule.id })) });
|
|
68
80
|
const summary = getSummaryLine(cwd, findings.length, format);
|
|
69
81
|
return {
|
|
70
|
-
content: [{ type: "text", text: results
|
|
82
|
+
content: [{ type: "text", text: mergeStatsIntoOutput(results, summary, format) }],
|
|
71
83
|
};
|
|
72
84
|
});
|
|
73
85
|
// Tool 2: Scan entire project for security vulnerabilities
|
|
@@ -100,7 +112,7 @@ server.tool("check_project", "Scan multiple files for security vulnerabilities a
|
|
|
100
112
|
}
|
|
101
113
|
const summary = getSummaryLine(cwd, findingCount, format);
|
|
102
114
|
return {
|
|
103
|
-
content: [{ type: "text", text: results
|
|
115
|
+
content: [{ type: "text", text: mergeStatsIntoOutput(results, summary, format) }],
|
|
104
116
|
};
|
|
105
117
|
});
|
|
106
118
|
// Tool 3: Get security documentation and best practices (renumbered from Tool 2)
|
|
@@ -146,7 +158,7 @@ server.tool("check_dependencies", "Check npm, PyPI, or Go packages for known sec
|
|
|
146
158
|
}
|
|
147
159
|
});
|
|
148
160
|
// Tool 5: Scan directory for security vulnerabilities (filesystem-native)
|
|
149
|
-
server.tool("scan_directory", "Scan
|
|
161
|
+
server.tool("scan_directory", "Scan all files in a directory on disk for security vulnerabilities. Pass a directory path — reads files from filesystem. Returns security score (A-F) and findings. Results may be truncated for large projects — check fileRanking in JSON output for top files. Example: scan_directory({path: './src'})", {
|
|
150
162
|
path: z.string().describe("Directory path to scan (e.g. './src', '.')"),
|
|
151
163
|
recursive: z.boolean().optional().default(true).describe("Scan subdirectories"),
|
|
152
164
|
exclude: z.array(z.string()).optional().default([]).describe("Additional directories to exclude"),
|
|
@@ -174,7 +186,7 @@ server.tool("scan_directory", "Scan an entire project directory for security vul
|
|
|
174
186
|
recordScan(root, { toolName: "scan_directory", filesScanned: 0, findings: [] });
|
|
175
187
|
}
|
|
176
188
|
const summary = getSummaryLine(root, findingCount, format);
|
|
177
|
-
return { content: [{ type: "text", text: results
|
|
189
|
+
return { content: [{ type: "text", text: mergeStatsIntoOutput(results, summary, format) }] };
|
|
178
190
|
});
|
|
179
191
|
// Tool 6: Scan manifest/lockfile for dependency vulnerabilities
|
|
180
192
|
server.tool("scan_dependencies", "Parse a lockfile or manifest (package.json, package-lock.json, requirements.txt, go.mod) and check all dependencies for known CVEs via the OSV database. Reads the file directly. Use this after installing dependencies, during CI, or when auditing existing projects for vulnerable packages.", {
|
|
@@ -239,10 +251,10 @@ server.tool("scan_staged", "Scan git-staged files for security vulnerabilities b
|
|
|
239
251
|
recordScan(cwd, { toolName: "scan_staged", filesScanned: 0, findings: [] });
|
|
240
252
|
}
|
|
241
253
|
const summary = getSummaryLine(cwd, findingCount, format);
|
|
242
|
-
return { content: [{ type: "text", text: results
|
|
254
|
+
return { content: [{ type: "text", text: mergeStatsIntoOutput(results, summary, format) }] };
|
|
243
255
|
});
|
|
244
256
|
// Tool 9: Generate compliance-focused security report
|
|
245
|
-
server.tool("compliance_report", "Map security
|
|
257
|
+
server.tool("compliance_report", "Map security findings to compliance controls (SOC2, PCI-DSS, HIPAA, GDPR, ISO27001, EUAIACT). Scans a directory and groups issues by control. Output includes a summary section at the top; for large projects, findings are truncated to top 50. Use mode=executive for C-level summary. Example: compliance_report({path: '.', framework: 'SOC2'})", {
|
|
246
258
|
path: z.string().describe("Directory to scan"),
|
|
247
259
|
framework: z.enum(["SOC2", "PCI-DSS", "HIPAA", "GDPR", "ISO27001", "EUAIACT", "all"]).describe("Compliance framework"),
|
|
248
260
|
format: z.enum(["markdown", "json"]).default("markdown").describe("Output format: markdown (human) or json (machine-readable for agents)"),
|
|
@@ -275,7 +287,7 @@ server.tool("check_package_health", "Check npm packages for typosquat risk, main
|
|
|
275
287
|
}
|
|
276
288
|
});
|
|
277
289
|
// Tool 12: Auto-fix security vulnerabilities
|
|
278
|
-
server.tool("fix_code", "
|
|
290
|
+
server.tool("fix_code", "Pass vulnerable code as a string and get fix suggestions with before/after patches. Returns structured edit instructions (line numbers, severity, confidence). Use verify_fix afterwards to confirm the fix resolved the issue. Example: fix_code({code: '...', language: 'typescript'})", {
|
|
279
291
|
code: z.string().describe("The code snippet to analyze and fix"),
|
|
280
292
|
language: z
|
|
281
293
|
.enum(["javascript", "typescript", "python", "go", "dockerfile", "html", "sql", "shell", "yaml", "terraform", "firestore"])
|
|
@@ -301,7 +313,7 @@ server.tool("fix_code", "Analyze code for security vulnerabilities and return fi
|
|
|
301
313
|
};
|
|
302
314
|
});
|
|
303
315
|
// Tool 13: Cross-file configuration security audit
|
|
304
|
-
server.tool("audit_config", "Audit
|
|
316
|
+
server.tool("audit_config", "Audit application config files (next.config, middleware, .env, vercel.json) for cross-file security gaps: missing headers, unprotected routes, exposed secrets. NOT the same as guardvibe_doctor which checks AI host security (MCP configs, hooks). Example: audit_config({path: '.'})", {
|
|
305
317
|
path: z.string().describe("Project root directory to audit"),
|
|
306
318
|
format: z.enum(["markdown", "json"]).default("markdown").describe("Output format"),
|
|
307
319
|
}, async ({ path, format }) => {
|
|
@@ -362,16 +374,66 @@ server.tool("analyze_dataflow", "Track user input (request body, URL params, for
|
|
|
362
374
|
return { content: [{ type: "text", text: results }] };
|
|
363
375
|
});
|
|
364
376
|
// Tool 18b: Cross-File Taint/Dataflow Analysis
|
|
365
|
-
server.tool("analyze_cross_file_dataflow", "Track user input flowing across module boundaries — detects injection vulnerabilities
|
|
377
|
+
server.tool("analyze_cross_file_dataflow", "Track user input flowing across module boundaries — detects injection vulnerabilities spanning multiple files. Pass files array with file contents. For single-file analysis, use analyze_dataflow instead. Example: analyze_cross_file_dataflow({files: [{path: 'src/api.ts', content: '...'}, {path: 'src/db.ts', content: '...'}]})", {
|
|
378
|
+
path: z.string().optional().describe("Project directory path. When provided, auto-discovers all JS/TS files — no need to pass file contents manually."),
|
|
366
379
|
files: z
|
|
367
380
|
.array(z.object({
|
|
368
381
|
path: z.string().describe("Relative file path (e.g. src/lib/db.ts)"),
|
|
369
382
|
content: z.string().describe("File source code"),
|
|
370
383
|
}))
|
|
371
|
-
.
|
|
384
|
+
.default([])
|
|
385
|
+
.describe("List of files to analyze (ignored when path is provided)"),
|
|
372
386
|
format: z.enum(["markdown", "json"]).default("markdown").describe("Output format"),
|
|
373
|
-
}, async ({ files, format }) => {
|
|
374
|
-
|
|
387
|
+
}, async ({ path, files, format }) => {
|
|
388
|
+
let analysisFiles = files;
|
|
389
|
+
if (path) {
|
|
390
|
+
const { readdirSync, readFileSync, statSync } = await import("fs");
|
|
391
|
+
const { resolve: resolvePath } = await import("path");
|
|
392
|
+
const jsFiles = [];
|
|
393
|
+
const skip = new Set(["node_modules", ".git", ".next", "build", "dist", ".turbo", "coverage"]);
|
|
394
|
+
function walk(d) {
|
|
395
|
+
if (jsFiles.length >= 500)
|
|
396
|
+
return;
|
|
397
|
+
let entries;
|
|
398
|
+
try {
|
|
399
|
+
entries = readdirSync(d);
|
|
400
|
+
}
|
|
401
|
+
catch {
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
for (const entry of entries) {
|
|
405
|
+
if (jsFiles.length >= 500)
|
|
406
|
+
return;
|
|
407
|
+
if (skip.has(entry))
|
|
408
|
+
continue;
|
|
409
|
+
const full = resolvePath(d, entry);
|
|
410
|
+
let stat;
|
|
411
|
+
try {
|
|
412
|
+
stat = statSync(full);
|
|
413
|
+
}
|
|
414
|
+
catch {
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
if (stat.isDirectory()) {
|
|
418
|
+
walk(full);
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
if (!/\.(ts|tsx|js|jsx)$/.test(entry))
|
|
422
|
+
continue;
|
|
423
|
+
if (stat.size > 100_000)
|
|
424
|
+
continue;
|
|
425
|
+
try {
|
|
426
|
+
const content = readFileSync(full, "utf-8");
|
|
427
|
+
const relPath = full.replace(resolvePath(path) + "/", "");
|
|
428
|
+
jsFiles.push({ path: relPath, content });
|
|
429
|
+
}
|
|
430
|
+
catch { /* skip unreadable */ }
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
walk(resolvePath(path));
|
|
434
|
+
analysisFiles = jsFiles;
|
|
435
|
+
}
|
|
436
|
+
const { crossFileFindings, perFileFindings } = analyzeCrossFileTaint(analysisFiles);
|
|
375
437
|
const total = crossFileFindings.length + Array.from(perFileFindings.values()).reduce((sum, f) => sum + f.length, 0);
|
|
376
438
|
if (total === 0) {
|
|
377
439
|
if (format === "json")
|
|
@@ -410,7 +472,7 @@ server.tool("repo_security_posture", "Analyze a repository's overall security po
|
|
|
410
472
|
return { content: [{ type: "text", text: results }] };
|
|
411
473
|
});
|
|
412
474
|
// Tool 22: Explain Remediation
|
|
413
|
-
server.tool("explain_remediation", "
|
|
475
|
+
server.tool("explain_remediation", "Pass a GuardVibe rule ID (e.g. VG154) to get a detailed explanation: risk assessment, exploit scenario, minimum fix, secure alternative, and test strategy. Optionally pass the affected code snippet for context-aware guidance. Example: explain_remediation({rule_id: 'VG402'})", {
|
|
414
476
|
rule_id: z.string().describe("GuardVibe rule ID (e.g. VG001, VG402)"),
|
|
415
477
|
code: z.string().optional().describe("Affected code snippet for context"),
|
|
416
478
|
format: z.enum(["markdown", "json"]).default("markdown").describe("Output format"),
|
|
@@ -420,7 +482,7 @@ server.tool("explain_remediation", "Deep explanation of a security finding: why
|
|
|
420
482
|
return { content: [{ type: "text", text: results }] };
|
|
421
483
|
});
|
|
422
484
|
// Tool 23: Quick file scan — designed for real-time integration
|
|
423
|
-
server.tool("scan_file", "Scan a single file
|
|
485
|
+
server.tool("scan_file", "Scan a single file on disk by path for security vulnerabilities. Pass a file path — the tool reads the file itself. For inline code snippets, use check_code instead. Example: scan_file({file_path: 'src/api/route.ts'})", {
|
|
424
486
|
file_path: z.string().describe("Absolute or relative path to the file to scan"),
|
|
425
487
|
format: z.enum(["markdown", "json"]).default("json").describe("Output format"),
|
|
426
488
|
}, async ({ file_path, format }) => {
|
|
@@ -459,9 +521,9 @@ server.tool("scan_file", "Scan a single file from disk for security vulnerabilit
|
|
|
459
521
|
confidence: f.confidence,
|
|
460
522
|
effort: f.effort,
|
|
461
523
|
})).filter((f) => f.edit);
|
|
462
|
-
return { content: [{ type: "text", text: JSON.stringify(parsed)
|
|
524
|
+
return { content: [{ type: "text", text: mergeStatsIntoOutput(JSON.stringify(parsed), summary, format) }] };
|
|
463
525
|
}
|
|
464
|
-
return { content: [{ type: "text", text: result
|
|
526
|
+
return { content: [{ type: "text", text: mergeStatsIntoOutput(result, summary, format) }] };
|
|
465
527
|
});
|
|
466
528
|
// Tool 24: Scan changed files only — for incremental CI/CD and PR workflows
|
|
467
529
|
server.tool("scan_changed_files", "Scan only files that have changed since a given git ref (branch, commit, or HEAD~N). Ideal for PR checks, pre-push hooks, and incremental CI. Returns findings only for modified/added files.", {
|
|
@@ -522,10 +584,10 @@ server.tool("scan_changed_files", "Scan only files that have changed since a giv
|
|
|
522
584
|
const critical = allFindings.filter(f => f.severity === "critical").length;
|
|
523
585
|
const high = allFindings.filter(f => f.severity === "high").length;
|
|
524
586
|
const medium = allFindings.filter(f => f.severity === "medium").length;
|
|
525
|
-
return { content: [{ type: "text", text: JSON.stringify({
|
|
587
|
+
return { content: [{ type: "text", text: mergeStatsIntoOutput(JSON.stringify({
|
|
526
588
|
summary: { total: allFindings.length, critical, high, medium, low: 0, blocked: critical > 0 || high > 0, changedFiles: changedFiles.length },
|
|
527
589
|
findings: allFindings,
|
|
528
|
-
})
|
|
590
|
+
}), statsSummary, format) }] };
|
|
529
591
|
}
|
|
530
592
|
// Markdown
|
|
531
593
|
const lines = [`# GuardVibe Changed Files Report`, ``, `Base: ${base}`, `Changed files: ${changedFiles.length}`, `Issues found: ${allFindings.length}`, ``];
|
|
@@ -539,7 +601,7 @@ server.tool("scan_changed_files", "Scan only files that have changed since a giv
|
|
|
539
601
|
lines.push(`- [${f.severity.toUpperCase()}] **${f.name}** (${f.id}) in \`${f.file}\`:${f.line} — ${f.fix}`);
|
|
540
602
|
}
|
|
541
603
|
}
|
|
542
|
-
return { content: [{ type: "text", text: lines.join("\n")
|
|
604
|
+
return { content: [{ type: "text", text: mergeStatsIntoOutput(lines.join("\n"), statsSummary, format) }] };
|
|
543
605
|
});
|
|
544
606
|
// Tool 25: Security statistics dashboard
|
|
545
607
|
server.tool("security_stats", "Show cumulative security statistics, grade trend, and vulnerability fix progress for this project. Use this to demonstrate the value of GuardVibe security scanning over time. Data is stored locally in .guardvibe/stats.json.", {
|
|
@@ -576,7 +638,7 @@ server.tool("scan_host_config", "Scan host environment for AI security issues: A
|
|
|
576
638
|
return { content: [{ type: "text", text: redactSecrets(output) }] };
|
|
577
639
|
});
|
|
578
640
|
// Tool 28: Unified host hardening scanner (doctor)
|
|
579
|
-
server.tool("guardvibe_doctor", "
|
|
641
|
+
server.tool("guardvibe_doctor", "Check AI host security: MCP configurations, hooks, base URL hijacking, environment variable exposure. NOT the same as audit_config which checks application config files (next.config, .env, headers). Use scope=project (default) for project-only, scope=host to include shell profiles and global AI configs. Example: guardvibe_doctor({scope: 'project'})", {
|
|
580
642
|
path: z.string().default(".").describe("Project root directory"),
|
|
581
643
|
scope: z.enum(["project", "host", "full"]).default("project").describe("Scan scope: project (default, .claude.json + .cursor/ + .vscode/ + .env), host (+ shell profiles + global MCP configs), full (+ home dir configs)"),
|
|
582
644
|
format: z.enum(["markdown", "json"]).default("markdown").describe("Output format: markdown (human) or json (machine-readable)"),
|
|
@@ -596,7 +658,7 @@ server.tool("verify_fix", "Verify that a specific security fix was applied corre
|
|
|
596
658
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
597
659
|
});
|
|
598
660
|
// Tool 30: Security workflow guide for AI agents
|
|
599
|
-
server.tool("security_workflow", "Get the recommended GuardVibe
|
|
661
|
+
server.tool("security_workflow", "Get the recommended GuardVibe tool sequence for your current task. Returns which tools to call, in what order, and with what parameters. Use this when unsure which tool to use. Example: security_workflow({task: 'pre_commit'})", {
|
|
600
662
|
task: z.enum([
|
|
601
663
|
"writing_code",
|
|
602
664
|
"pre_commit",
|
|
@@ -609,7 +671,7 @@ server.tool("security_workflow", "Get the recommended GuardVibe security workflo
|
|
|
609
671
|
"publish_package",
|
|
610
672
|
"security_audit",
|
|
611
673
|
"incident_response",
|
|
612
|
-
]).describe("
|
|
674
|
+
]).describe("Current task: writing_code (after edits), pre_commit (before commit), pr_review (reviewing PR), new_project (initial setup), fix_vulnerabilities (fixing known issues), compliance_mapping (audit against framework), dependency_check (check deps), merge_to_main (pre-merge gate), publish_package (pre-publish checks), security_audit (comprehensive audit), incident_response (post-breach investigation)"),
|
|
613
675
|
}, async ({ task }) => {
|
|
614
676
|
const workflows = {
|
|
615
677
|
writing_code: {
|
|
@@ -715,14 +777,67 @@ server.tool("security_workflow", "Get the recommended GuardVibe security workflo
|
|
|
715
777
|
return { content: [{ type: "text", text: JSON.stringify(workflows[task]) }] };
|
|
716
778
|
});
|
|
717
779
|
// Tool 31: Auth coverage map
|
|
718
|
-
server.tool("auth_coverage", "Analyze authentication coverage across
|
|
780
|
+
server.tool("auth_coverage", "Analyze authentication coverage across Next.js App Router routes. Detects auth guards (Clerk, NextAuth, Supabase, custom) and reports protected vs unprotected routes. Pass files array with route file contents and middleware content. Example: auth_coverage({files: [{path: 'app/api/users/route.ts', content: '...'}], middleware: '...'})", {
|
|
781
|
+
path: z.string().optional().describe("Project directory path. When provided, auto-discovers all route, page, layout, and middleware files — no need to pass file contents manually."),
|
|
719
782
|
files: z.array(z.object({
|
|
720
783
|
path: z.string().describe("File path relative to project root (e.g. app/api/users/route.ts)"),
|
|
721
784
|
content: z.string().describe("File source code"),
|
|
722
|
-
})).describe("Route and page files from app/ directory"),
|
|
723
|
-
middleware: z.string().default("").describe("Content of middleware.ts file (
|
|
785
|
+
})).default([]).describe("Route and page files from app/ directory (ignored when path is provided)"),
|
|
786
|
+
middleware: z.string().default("").describe("Content of middleware.ts file (ignored when path is provided)"),
|
|
724
787
|
format: z.enum(["markdown", "json"]).default("markdown").describe("Output format"),
|
|
725
|
-
}, async ({ files, middleware, format }) => {
|
|
788
|
+
}, async ({ path, files, middleware, format }) => {
|
|
789
|
+
if (path) {
|
|
790
|
+
const { readdirSync, readFileSync, statSync } = await import("fs");
|
|
791
|
+
const { resolve: resolvePath } = await import("path");
|
|
792
|
+
const jsFiles = [];
|
|
793
|
+
const skip = new Set(["node_modules", ".git", ".next", "build", "dist", ".turbo", "coverage"]);
|
|
794
|
+
function walk(d) {
|
|
795
|
+
if (jsFiles.length >= 500)
|
|
796
|
+
return;
|
|
797
|
+
let entries;
|
|
798
|
+
try {
|
|
799
|
+
entries = readdirSync(d);
|
|
800
|
+
}
|
|
801
|
+
catch {
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
for (const entry of entries) {
|
|
805
|
+
if (jsFiles.length >= 500)
|
|
806
|
+
return;
|
|
807
|
+
if (skip.has(entry))
|
|
808
|
+
continue;
|
|
809
|
+
const full = resolvePath(d, entry);
|
|
810
|
+
let stat;
|
|
811
|
+
try {
|
|
812
|
+
stat = statSync(full);
|
|
813
|
+
}
|
|
814
|
+
catch {
|
|
815
|
+
continue;
|
|
816
|
+
}
|
|
817
|
+
if (stat.isDirectory()) {
|
|
818
|
+
walk(full);
|
|
819
|
+
continue;
|
|
820
|
+
}
|
|
821
|
+
if (!/\.(ts|tsx|js|jsx)$/.test(entry))
|
|
822
|
+
continue;
|
|
823
|
+
if (stat.size > 100_000)
|
|
824
|
+
continue;
|
|
825
|
+
try {
|
|
826
|
+
const content = readFileSync(full, "utf-8");
|
|
827
|
+
const relPath = full.replace(resolvePath(path) + "/", "");
|
|
828
|
+
jsFiles.push({ path: relPath, content });
|
|
829
|
+
}
|
|
830
|
+
catch { /* skip unreadable */ }
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
walk(resolvePath(path));
|
|
834
|
+
const routeFiles = jsFiles.filter(f => /\/(route|page)\.(ts|tsx|js|jsx)$/.test(f.path));
|
|
835
|
+
const layoutFiles = jsFiles.filter(f => /\/layout\.(ts|tsx|js|jsx)$/.test(f.path));
|
|
836
|
+
const middlewareFile = jsFiles.find(f => /middleware\.(ts|js)$/.test(f.path));
|
|
837
|
+
const report = analyzeAuthCoverage(routeFiles, middlewareFile?.content ?? "", layoutFiles);
|
|
838
|
+
const output = formatAuthCoverage(report, format);
|
|
839
|
+
return { content: [{ type: "text", text: output }] };
|
|
840
|
+
}
|
|
726
841
|
const report = analyzeAuthCoverage(files, middleware);
|
|
727
842
|
const output = formatAuthCoverage(report, format);
|
|
728
843
|
return { content: [{ type: "text", text: output }] };
|
|
@@ -750,7 +865,7 @@ server.tool("deep_scan", "LLM-powered deep security analysis for vulnerabilities
|
|
|
750
865
|
return { content: [{ type: "text", text: output }] };
|
|
751
866
|
});
|
|
752
867
|
// Tool 33: Full audit — single source of truth
|
|
753
|
-
server.tool("full_audit", "Single
|
|
868
|
+
server.tool("full_audit", "Single command that runs ALL checks: code scan (334 rules), secret detection, dependency CVEs, config audit, taint analysis, and auth coverage. Returns PASS/FAIL/WARN verdict with deterministic hash. Use this for comprehensive security assessment — no need to call individual tools separately. Example: full_audit({path: '.'})", {
|
|
754
869
|
path: z.string().default(".").describe("Project root directory"),
|
|
755
870
|
format: z.enum(["markdown", "json"]).default("markdown").describe("Output format"),
|
|
756
871
|
skipDeps: z.boolean().default(false).describe("Skip dependency vulnerability check"),
|
|
@@ -79,10 +79,12 @@ export function enumerateRoutes(files) {
|
|
|
79
79
|
* Returns array of matcher patterns.
|
|
80
80
|
*/
|
|
81
81
|
export function parseMiddlewareMatchers(content) {
|
|
82
|
-
|
|
82
|
+
// Normalize literal escape sequences that AI assistants may pass
|
|
83
|
+
const normalized = content.replace(/\\n/g, "\n").replace(/\\t/g, "\t");
|
|
84
|
+
const stringMatch = /matcher\s*:\s*"([^"]+)"/.exec(normalized);
|
|
83
85
|
if (stringMatch)
|
|
84
86
|
return [stringMatch[1]];
|
|
85
|
-
const arrayMatch = /matcher\s*:\s*\[([^\]]+)\]/.exec(
|
|
87
|
+
const arrayMatch = /matcher\s*:\s*\[([^\]]+)\]/.exec(normalized);
|
|
86
88
|
if (arrayMatch) {
|
|
87
89
|
return arrayMatch[1]
|
|
88
90
|
.split(",")
|
|
@@ -63,12 +63,37 @@ export function complianceReport(path, framework, format = "markdown", rules, mo
|
|
|
63
63
|
const critical = relevant.filter(f => f.rule.severity === "critical").length;
|
|
64
64
|
const high = relevant.filter(f => f.rule.severity === "high").length;
|
|
65
65
|
const medium = relevant.filter(f => f.rule.severity === "medium").length;
|
|
66
|
+
// Count controls that have zero findings as "passed"
|
|
67
|
+
const totalControls = controlMap.size;
|
|
68
|
+
const failedControls = totalControls; // all mapped controls have findings
|
|
69
|
+
const passedControls = 0; // controls without findings are not in the map
|
|
70
|
+
const complianceScore = totalControls === 0 ? 100 : Math.round((passedControls / totalControls) * 100);
|
|
71
|
+
// Flatten all findings, sort by severity, limit to top 50
|
|
72
|
+
const severityOrder = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
|
|
73
|
+
const allFindingsFlat = relevant
|
|
74
|
+
.map(f => ({
|
|
75
|
+
id: f.rule.id, name: f.rule.name, severity: f.rule.severity,
|
|
76
|
+
file: f.filePath, line: f.line,
|
|
77
|
+
exploit: f.rule.exploit, audit: f.rule.audit,
|
|
78
|
+
}))
|
|
79
|
+
.sort((a, b) => (severityOrder[a.severity] ?? 4) - (severityOrder[b.severity] ?? 4));
|
|
80
|
+
const MAX_JSON_FINDINGS = 50;
|
|
81
|
+
const truncated = allFindingsFlat.length > MAX_JSON_FINDINGS;
|
|
82
|
+
const topFindings = allFindingsFlat.slice(0, MAX_JSON_FINDINGS);
|
|
66
83
|
return JSON.stringify({
|
|
67
84
|
summary: {
|
|
68
|
-
framework,
|
|
69
|
-
|
|
85
|
+
framework,
|
|
86
|
+
totalControls,
|
|
87
|
+
passedControls,
|
|
88
|
+
failedControls,
|
|
89
|
+
totalFindings: relevant.length,
|
|
90
|
+
critical, high, medium,
|
|
91
|
+
complianceScore,
|
|
92
|
+
mode,
|
|
70
93
|
},
|
|
71
94
|
controls,
|
|
95
|
+
findings: topFindings,
|
|
96
|
+
...(truncated ? { truncation: { shown: MAX_JSON_FINDINGS, total: allFindingsFlat.length, note: "Sorted by severity. Use scan_file on individual files for full details." } } : {}),
|
|
72
97
|
});
|
|
73
98
|
}
|
|
74
99
|
// --- EXECUTIVE SUMMARY MODE ---
|
|
@@ -73,7 +73,13 @@ export async function scanDependencies(manifestPath, format = "markdown") {
|
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
75
|
return JSON.stringify({
|
|
76
|
-
summary: {
|
|
76
|
+
summary: {
|
|
77
|
+
total: packages.length,
|
|
78
|
+
vulnerable: criticalPackages.length,
|
|
79
|
+
vulnerablePackages: criticalPackages.length,
|
|
80
|
+
totalAdvisories: totalVulns,
|
|
81
|
+
...sevCounts,
|
|
82
|
+
},
|
|
77
83
|
packages: pkgResults,
|
|
78
84
|
});
|
|
79
85
|
}
|
|
@@ -95,6 +95,15 @@ export function scanDirectory(path, recursive = true, exclude = [], format = "ma
|
|
|
95
95
|
filesSkipped: skippedFiles.length,
|
|
96
96
|
fileHashes,
|
|
97
97
|
};
|
|
98
|
+
// File ranking — always included in JSON, even when truncated
|
|
99
|
+
const fileRankingMap = new Map();
|
|
100
|
+
for (const r of scanResults) {
|
|
101
|
+
fileRankingMap.set(r.path, r.findings.length);
|
|
102
|
+
}
|
|
103
|
+
const fileRanking = Array.from(fileRankingMap.entries())
|
|
104
|
+
.sort((a, b) => b[1] - a[1])
|
|
105
|
+
.slice(0, 10)
|
|
106
|
+
.map(([file, count]) => ({ file, findingCount: count }));
|
|
98
107
|
// Scoring
|
|
99
108
|
const allFindings = scanResults.flatMap(r => r.findings);
|
|
100
109
|
const totalCritical = allFindings.filter(f => f.rule.severity === "critical").length;
|
|
@@ -155,6 +164,7 @@ export function scanDirectory(path, recursive = true, exclude = [], format = "ma
|
|
|
155
164
|
},
|
|
156
165
|
},
|
|
157
166
|
metadata,
|
|
167
|
+
fileRanking,
|
|
158
168
|
findings: limitedFindings.map(f => ({
|
|
159
169
|
id: f.rule.id, name: f.rule.name, severity: f.rule.severity,
|
|
160
170
|
owasp: f.rule.owasp, line: f.line, match: f.match, file: f.file,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "guardvibe",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.6",
|
|
4
4
|
"mcpName": "io.github.goklab/guardvibe",
|
|
5
5
|
"description": "Security MCP for vibe coding. 334 rules, 34 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",
|