guardvibe 2.5.0 → 2.7.4
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/CHANGELOG.md +35 -0
- package/README.md +43 -14
- package/build/cli/args.d.ts +15 -0
- package/build/cli/args.js +78 -0
- package/build/cli/ci.d.ts +5 -0
- package/build/cli/ci.js +67 -0
- package/build/cli/doctor.d.ts +5 -0
- package/build/cli/doctor.js +35 -0
- package/build/cli/hook.d.ts +5 -0
- package/build/cli/hook.js +90 -0
- package/build/cli/init.d.ts +5 -0
- package/build/cli/init.js +214 -0
- package/build/cli/remediation.d.ts +14 -0
- package/build/cli/remediation.js +91 -0
- package/build/cli/scan.d.ts +11 -0
- package/build/cli/scan.js +227 -0
- package/build/cli.js +33 -639
- package/build/data/rules/ai-host-security.d.ts +2 -0
- package/build/data/rules/ai-host-security.js +128 -0
- package/build/data/rules/ai-tool-runtime.d.ts +2 -0
- package/build/data/rules/ai-tool-runtime.js +54 -0
- package/build/data/rules/index.js +4 -0
- package/build/index.js +36 -0
- package/build/server/register.d.ts +7 -0
- package/build/server/register.js +7 -0
- package/build/server/types.d.ts +40 -0
- package/build/server/types.js +138 -0
- package/build/tools/audit-mcp-config.d.ts +10 -0
- package/build/tools/audit-mcp-config.js +296 -0
- package/build/tools/check-code.js +3 -0
- package/build/tools/doctor.d.ts +14 -0
- package/build/tools/doctor.js +123 -0
- package/build/tools/scan-directory.js +2 -0
- package/build/tools/scan-host-config.d.ts +10 -0
- package/build/tools/scan-host-config.js +181 -0
- package/build/tools/scan-staged.js +2 -0
- package/build/utils/banner.d.ts +36 -0
- package/build/utils/banner.js +66 -0
- package/package.json +2 -3
package/build/cli.js
CHANGED
|
@@ -1,580 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from "module";
|
|
3
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync, unlinkSync, statSync } from "fs";
|
|
4
|
-
import { join, dirname } from "path";
|
|
5
|
-
import { homedir } from "os";
|
|
6
3
|
const require = createRequire(import.meta.url);
|
|
7
4
|
const pkg = require("../package.json");
|
|
8
|
-
|
|
9
|
-
command: "npx",
|
|
10
|
-
args: ["-y", "guardvibe"],
|
|
11
|
-
};
|
|
12
|
-
const platforms = {
|
|
13
|
-
claude: {
|
|
14
|
-
path: join(process.cwd(), ".claude.json"),
|
|
15
|
-
description: "Claude Code (.claude.json)",
|
|
16
|
-
},
|
|
17
|
-
gemini: {
|
|
18
|
-
path: join(homedir(), ".gemini", "settings.json"),
|
|
19
|
-
description: "Gemini CLI (~/.gemini/settings.json)",
|
|
20
|
-
},
|
|
21
|
-
cursor: {
|
|
22
|
-
path: join(process.cwd(), ".cursor", "mcp.json"),
|
|
23
|
-
description: "Cursor (.cursor/mcp.json)",
|
|
24
|
-
},
|
|
25
|
-
};
|
|
26
|
-
function readJsonFile(filePath) {
|
|
27
|
-
try {
|
|
28
|
-
const content = readFileSync(filePath, "utf-8");
|
|
29
|
-
return JSON.parse(content);
|
|
30
|
-
}
|
|
31
|
-
catch {
|
|
32
|
-
return null;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
function writeJsonFile(filePath, data) {
|
|
36
|
-
const dir = dirname(filePath);
|
|
37
|
-
if (!existsSync(dir)) {
|
|
38
|
-
mkdirSync(dir, { recursive: true });
|
|
39
|
-
}
|
|
40
|
-
writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
41
|
-
}
|
|
42
|
-
function setupPlatform(name) {
|
|
43
|
-
const platform = platforms[name];
|
|
44
|
-
if (!platform)
|
|
45
|
-
return false;
|
|
46
|
-
const existing = readJsonFile(platform.path);
|
|
47
|
-
if (existing) {
|
|
48
|
-
if (!existing.mcpServers) {
|
|
49
|
-
existing.mcpServers = {};
|
|
50
|
-
}
|
|
51
|
-
if (existing.mcpServers["guardvibe"]) {
|
|
52
|
-
console.log(` [OK] GuardVibe already configured in ${platform.description}`);
|
|
53
|
-
// Still ensure security guidance and .gitignore are set up
|
|
54
|
-
setupSecurityGuide(name);
|
|
55
|
-
return true;
|
|
56
|
-
}
|
|
57
|
-
existing.mcpServers["guardvibe"] = GUARDVIBE_MCP_CONFIG;
|
|
58
|
-
writeJsonFile(platform.path, existing);
|
|
59
|
-
}
|
|
60
|
-
else {
|
|
61
|
-
writeJsonFile(platform.path, {
|
|
62
|
-
mcpServers: {
|
|
63
|
-
guardvibe: GUARDVIBE_MCP_CONFIG,
|
|
64
|
-
},
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
console.log(` [OK] Added MCP server to ${platform.description}`);
|
|
68
|
-
// Set up platform-specific security guidance + universal security rules
|
|
69
|
-
setupSecurityGuide(name);
|
|
70
|
-
return true;
|
|
71
|
-
}
|
|
72
|
-
// ── Universal security rules (shared across all AI platforms) ────────
|
|
73
|
-
const SECURITY_RULES = `## Security Rules (enforced by GuardVibe MCP)
|
|
74
|
-
|
|
75
|
-
### Mandatory checks
|
|
76
|
-
- AFTER writing any API route, Server Action, or database query: use the guardvibe scan_file MCP tool on the file
|
|
77
|
-
- BEFORE committing: use the guardvibe scan_staged MCP tool
|
|
78
|
-
- When GuardVibe reports HIGH or CRITICAL: fix immediately before moving on
|
|
79
|
-
- BEFORE creating a PR: use scan_changed_files to review all changes
|
|
80
|
-
|
|
81
|
-
### Scan visibility — ALWAYS report GuardVibe results to the user
|
|
82
|
-
- AFTER every scan: ALWAYS tell the user the GuardVibe result, even when clean
|
|
83
|
-
- When issues found: "GuardVibe: [count] issues found ([severity]). Fixing..."
|
|
84
|
-
- When clean: "GuardVibe: scanned [filename] — no issues found."
|
|
85
|
-
- Include the summary stats line from scan output when present
|
|
86
|
-
- NEVER silently consume scan results — the user must see that GuardVibe is protecting their code
|
|
87
|
-
- Periodically (every ~10 scans or at session start): run security_stats tool and share the dashboard with the user
|
|
88
|
-
|
|
89
|
-
### Writing secure code
|
|
90
|
-
- Every API route handler MUST have auth check before DB access
|
|
91
|
-
- Every POST endpoint MUST have input validation (zod/joi schema)
|
|
92
|
-
- Never pass user input to innerHTML, eval, exec, or raw SQL
|
|
93
|
-
- Never pass user-controlled URLs to fetch() without allowlist validation
|
|
94
|
-
- Always use select: in Prisma/Drizzle queries from Server Actions (never return full objects to client)
|
|
95
|
-
- Always validate redirect URLs against trusted domain allowlist
|
|
96
|
-
- Set security headers (CSP, HSTS, X-Frame-Options, X-Content-Type-Options)
|
|
97
|
-
- Verify webhook signatures before processing events
|
|
98
|
-
- Use parameterized queries, never string concatenation/template literals for SQL
|
|
99
|
-
|
|
100
|
-
### When in doubt
|
|
101
|
-
- Use the guardvibe explain_remediation MCP tool with the rule ID for detailed fix guidance
|
|
102
|
-
- Use the guardvibe check_code MCP tool to verify a code snippet is secure before applying
|
|
103
|
-
`;
|
|
104
|
-
function setupSecurityGuide(platformName) {
|
|
105
|
-
// 1. Platform-specific guidance file
|
|
106
|
-
if (platformName === "claude")
|
|
107
|
-
setupClaudeGuide();
|
|
108
|
-
else if (platformName === "cursor")
|
|
109
|
-
setupCursorGuide();
|
|
110
|
-
else if (platformName === "gemini")
|
|
111
|
-
setupGeminiGuide();
|
|
112
|
-
// 2. Platform-specific .gitignore entries
|
|
113
|
-
const gitignoreEntries = {
|
|
114
|
-
claude: [".claude.json", ".claude/", "CLAUDE.md"],
|
|
115
|
-
cursor: [".cursor/", ".cursorrules"],
|
|
116
|
-
gemini: ["GEMINI.md"],
|
|
117
|
-
};
|
|
118
|
-
const entries = gitignoreEntries[platformName] || [];
|
|
119
|
-
// Always add .guardvibe/ (stats directory) to .gitignore
|
|
120
|
-
entries.push(".guardvibe/");
|
|
121
|
-
if (entries.length > 0)
|
|
122
|
-
addToGitignore(entries);
|
|
123
|
-
}
|
|
124
|
-
function setupClaudeGuide() {
|
|
125
|
-
// Claude Code hooks
|
|
126
|
-
const claudeSettingsDir = join(process.cwd(), ".claude");
|
|
127
|
-
if (!existsSync(claudeSettingsDir))
|
|
128
|
-
mkdirSync(claudeSettingsDir, { recursive: true });
|
|
129
|
-
const claudeSettingsPath = join(claudeSettingsDir, "settings.json");
|
|
130
|
-
const existingSettings = readJsonFile(claudeSettingsPath) || {};
|
|
131
|
-
if (!existingSettings.hooks)
|
|
132
|
-
existingSettings.hooks = {};
|
|
133
|
-
if (!existingSettings.hooks.PostToolUse) {
|
|
134
|
-
existingSettings.hooks.PostToolUse = [
|
|
135
|
-
{
|
|
136
|
-
matcher: "Edit|Write",
|
|
137
|
-
hooks: [{
|
|
138
|
-
type: "command",
|
|
139
|
-
command: "jq -r '.tool_input.file_path' | xargs npx -y guardvibe check --format buddy 2>/dev/null || true"
|
|
140
|
-
}]
|
|
141
|
-
}
|
|
142
|
-
];
|
|
143
|
-
}
|
|
144
|
-
writeJsonFile(claudeSettingsPath, existingSettings);
|
|
145
|
-
console.log(` [OK] Claude Code hooks configured (.claude/settings.json)`);
|
|
146
|
-
// CLAUDE.md
|
|
147
|
-
const claudeMdPath = join(process.cwd(), "CLAUDE.md");
|
|
148
|
-
if (existsSync(claudeMdPath)) {
|
|
149
|
-
const content = readFileSync(claudeMdPath, "utf-8");
|
|
150
|
-
if (!content.includes("GuardVibe")) {
|
|
151
|
-
writeFileSync(claudeMdPath, content + "\n" + SECURITY_RULES, "utf-8");
|
|
152
|
-
console.log(` [OK] GuardVibe rules added to CLAUDE.md`);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
else {
|
|
156
|
-
writeFileSync(claudeMdPath, `# Project Guidelines\n\n${SECURITY_RULES}`, "utf-8");
|
|
157
|
-
console.log(` [OK] Created CLAUDE.md with security rules`);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
function setupCursorGuide() {
|
|
161
|
-
// .cursorrules — Cursor reads this file for AI instructions
|
|
162
|
-
const cursorrules = join(process.cwd(), ".cursorrules");
|
|
163
|
-
if (existsSync(cursorrules)) {
|
|
164
|
-
const content = readFileSync(cursorrules, "utf-8");
|
|
165
|
-
if (!content.includes("GuardVibe")) {
|
|
166
|
-
writeFileSync(cursorrules, content + "\n" + SECURITY_RULES, "utf-8");
|
|
167
|
-
console.log(` [OK] GuardVibe rules added to .cursorrules`);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
else {
|
|
171
|
-
writeFileSync(cursorrules, SECURITY_RULES, "utf-8");
|
|
172
|
-
console.log(` [OK] Created .cursorrules with security rules`);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
function setupGeminiGuide() {
|
|
176
|
-
// GEMINI.md — Gemini CLI reads this for project context
|
|
177
|
-
const geminiMd = join(process.cwd(), "GEMINI.md");
|
|
178
|
-
if (existsSync(geminiMd)) {
|
|
179
|
-
const content = readFileSync(geminiMd, "utf-8");
|
|
180
|
-
if (!content.includes("GuardVibe")) {
|
|
181
|
-
writeFileSync(geminiMd, content + "\n" + SECURITY_RULES, "utf-8");
|
|
182
|
-
console.log(` [OK] GuardVibe rules added to GEMINI.md`);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
else {
|
|
186
|
-
writeFileSync(geminiMd, `# Project Guidelines\n\n${SECURITY_RULES}`, "utf-8");
|
|
187
|
-
console.log(` [OK] Created GEMINI.md with security rules`);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
function addToGitignore(entries) {
|
|
191
|
-
const gitignorePath = join(process.cwd(), ".gitignore");
|
|
192
|
-
let content = "";
|
|
193
|
-
try {
|
|
194
|
-
content = readFileSync(gitignorePath, "utf-8");
|
|
195
|
-
}
|
|
196
|
-
catch { /* no .gitignore yet */ }
|
|
197
|
-
const missing = entries.filter(e => !content.split("\n").some(line => line.trim() === e));
|
|
198
|
-
if (missing.length === 0)
|
|
199
|
-
return;
|
|
200
|
-
const block = `\n# GuardVibe (auto-added by guardvibe init)\n${missing.join("\n")}\n`;
|
|
201
|
-
writeFileSync(gitignorePath, content.trimEnd() + block, "utf-8");
|
|
202
|
-
console.log(` [OK] Added ${missing.join(", ")} to .gitignore`);
|
|
203
|
-
}
|
|
204
|
-
// ── Pre-commit hook ──────────────────────────────────────────────────
|
|
205
|
-
const HOOK_SCRIPT = `#!/bin/sh
|
|
206
|
-
# GuardVibe pre-commit security hook
|
|
207
|
-
# Installed by: npx guardvibe hook install
|
|
208
|
-
|
|
209
|
-
echo "🔒 GuardVibe: scanning staged files..."
|
|
210
|
-
|
|
211
|
-
# Run guardvibe scan on staged files
|
|
212
|
-
RESULT=$(npx -y guardvibe-scan 2>&1)
|
|
213
|
-
EXIT_CODE=$?
|
|
214
|
-
|
|
215
|
-
if [ $EXIT_CODE -ne 0 ]; then
|
|
216
|
-
echo ""
|
|
217
|
-
echo "$RESULT"
|
|
218
|
-
echo ""
|
|
219
|
-
echo "❌ GuardVibe: security issues found. Fix them or commit with --no-verify to skip."
|
|
220
|
-
exit 1
|
|
221
|
-
fi
|
|
222
|
-
|
|
223
|
-
echo "✅ GuardVibe: all checks passed."
|
|
224
|
-
`;
|
|
225
|
-
function installHook() {
|
|
226
|
-
const gitDir = join(process.cwd(), ".git");
|
|
227
|
-
if (!existsSync(gitDir)) {
|
|
228
|
-
console.error(" [ERR] Not a git repository. Run this from your project root.");
|
|
229
|
-
process.exit(1);
|
|
230
|
-
}
|
|
231
|
-
const hooksDir = join(gitDir, "hooks");
|
|
232
|
-
if (!existsSync(hooksDir)) {
|
|
233
|
-
mkdirSync(hooksDir, { recursive: true });
|
|
234
|
-
}
|
|
235
|
-
const hookPath = join(hooksDir, "pre-commit");
|
|
236
|
-
if (existsSync(hookPath)) {
|
|
237
|
-
const existing = readFileSync(hookPath, "utf-8");
|
|
238
|
-
if (existing.includes("GuardVibe")) {
|
|
239
|
-
console.log(" [OK] GuardVibe pre-commit hook already installed.");
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
// Append to existing hook
|
|
243
|
-
writeFileSync(hookPath, existing + "\n" + HOOK_SCRIPT, "utf-8");
|
|
244
|
-
console.log(" [OK] GuardVibe added to existing pre-commit hook.");
|
|
245
|
-
}
|
|
246
|
-
else {
|
|
247
|
-
writeFileSync(hookPath, HOOK_SCRIPT, "utf-8");
|
|
248
|
-
chmodSync(hookPath, 0o755);
|
|
249
|
-
console.log(" [OK] Pre-commit hook installed at .git/hooks/pre-commit");
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
function uninstallHook() {
|
|
253
|
-
const hookPath = join(process.cwd(), ".git", "hooks", "pre-commit");
|
|
254
|
-
if (!existsSync(hookPath)) {
|
|
255
|
-
console.log(" [OK] No pre-commit hook found.");
|
|
256
|
-
return;
|
|
257
|
-
}
|
|
258
|
-
const content = readFileSync(hookPath, "utf-8");
|
|
259
|
-
if (!content.includes("GuardVibe")) {
|
|
260
|
-
console.log(" [OK] Pre-commit hook exists but doesn't contain GuardVibe.");
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
// Remove GuardVibe section
|
|
264
|
-
const cleaned = content
|
|
265
|
-
.replace(/\n?# GuardVibe pre-commit security hook[\s\S]*?GuardVibe: all checks passed[."]*\n?/g, "")
|
|
266
|
-
.trim();
|
|
267
|
-
if (!cleaned || cleaned === "#!/bin/sh") {
|
|
268
|
-
unlinkSync(hookPath);
|
|
269
|
-
console.log(" [OK] Pre-commit hook removed.");
|
|
270
|
-
}
|
|
271
|
-
else {
|
|
272
|
-
writeFileSync(hookPath, cleaned + "\n", "utf-8");
|
|
273
|
-
console.log(" [OK] GuardVibe removed from pre-commit hook (other hooks preserved).");
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
// ── GitHub Actions workflow ──────────────────────────────────────────
|
|
277
|
-
const GITHUB_ACTIONS_WORKFLOW = `name: GuardVibe Security Scan
|
|
278
|
-
|
|
279
|
-
on:
|
|
280
|
-
pull_request:
|
|
281
|
-
branches: [main, master]
|
|
282
|
-
push:
|
|
283
|
-
branches: [main, master]
|
|
284
|
-
|
|
285
|
-
permissions:
|
|
286
|
-
contents: read
|
|
287
|
-
security-events: write
|
|
288
|
-
|
|
289
|
-
jobs:
|
|
290
|
-
security-scan:
|
|
291
|
-
name: Security Scan
|
|
292
|
-
runs-on: ubuntu-latest
|
|
293
|
-
steps:
|
|
294
|
-
- uses: actions/checkout@v4
|
|
295
|
-
with:
|
|
296
|
-
persist-credentials: false
|
|
297
|
-
|
|
298
|
-
- uses: actions/setup-node@v4
|
|
299
|
-
with:
|
|
300
|
-
node-version: "22"
|
|
301
|
-
|
|
302
|
-
- name: Run GuardVibe security scan
|
|
303
|
-
run: npx -y guardvibe-scan --format sarif --output guardvibe-results.sarif
|
|
304
|
-
|
|
305
|
-
- name: Upload SARIF to GitHub Security
|
|
306
|
-
if: always()
|
|
307
|
-
uses: github/codeql-action/upload-sarif@v3
|
|
308
|
-
with:
|
|
309
|
-
sarif_file: guardvibe-results.sarif
|
|
310
|
-
category: guardvibe
|
|
311
|
-
`;
|
|
312
|
-
function generateGitHubActions() {
|
|
313
|
-
const workflowDir = join(process.cwd(), ".github", "workflows");
|
|
314
|
-
if (!existsSync(workflowDir)) {
|
|
315
|
-
mkdirSync(workflowDir, { recursive: true });
|
|
316
|
-
}
|
|
317
|
-
const workflowPath = join(workflowDir, "guardvibe.yml");
|
|
318
|
-
if (existsSync(workflowPath)) {
|
|
319
|
-
console.log(" [OK] .github/workflows/guardvibe.yml already exists.");
|
|
320
|
-
return;
|
|
321
|
-
}
|
|
322
|
-
writeFileSync(workflowPath, GITHUB_ACTIONS_WORKFLOW, "utf-8");
|
|
323
|
-
console.log(" [OK] Created .github/workflows/guardvibe.yml");
|
|
324
|
-
console.log(" [OK] SARIF results will appear in GitHub Security tab.");
|
|
325
|
-
}
|
|
326
|
-
// ── Scan CLI (for pre-commit hook and CI) ────────────────────────────
|
|
5
|
+
// ── Scan entry point detection ──────────────────────────────────────
|
|
327
6
|
const SCAN_SCRIPT_DETECTED = process.argv[1]?.endsWith("guardvibe-scan") ||
|
|
328
7
|
process.argv[1]?.endsWith("guardvibe-scan.js");
|
|
329
8
|
if (SCAN_SCRIPT_DETECTED) {
|
|
330
|
-
runScan();
|
|
9
|
+
const { runScan } = await import("./cli/scan.js");
|
|
10
|
+
await runScan();
|
|
331
11
|
}
|
|
332
12
|
else {
|
|
333
|
-
main();
|
|
334
|
-
}
|
|
335
|
-
/**
|
|
336
|
-
* Check if scan results should cause a non-zero exit based on --fail-on flag.
|
|
337
|
-
* Default: "critical" — only exit 1 on critical findings.
|
|
338
|
-
* Options: "critical", "high", "medium", "low", "none"
|
|
339
|
-
*/
|
|
340
|
-
function shouldFail(result, failOn) {
|
|
341
|
-
if (failOn === "none")
|
|
342
|
-
return false;
|
|
343
|
-
const levels = {
|
|
344
|
-
low: ["critical", "high", "medium", "low"],
|
|
345
|
-
medium: ["critical", "high", "medium"],
|
|
346
|
-
high: ["critical", "high"],
|
|
347
|
-
critical: ["critical"],
|
|
348
|
-
};
|
|
349
|
-
const failLevels = levels[failOn] || levels.critical;
|
|
350
|
-
// Try JSON format first
|
|
351
|
-
try {
|
|
352
|
-
const parsed = JSON.parse(result);
|
|
353
|
-
if (parsed.summary) {
|
|
354
|
-
return failLevels.some(level => (parsed.summary[level] ?? 0) > 0);
|
|
355
|
-
}
|
|
356
|
-
if (parsed.findings) {
|
|
357
|
-
return parsed.findings.some((f) => failLevels.includes(f.severity));
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
catch { /* not JSON, try markdown tags */ }
|
|
361
|
-
// Markdown format: check for [SEVERITY] tags
|
|
362
|
-
const tags = failLevels.map(l => `[${l.toUpperCase()}]`);
|
|
363
|
-
return tags.some(tag => result.includes(tag));
|
|
13
|
+
await main();
|
|
364
14
|
}
|
|
365
|
-
|
|
366
|
-
const flags = {};
|
|
367
|
-
const positional = [];
|
|
368
|
-
for (let i = 0; i < args.length; i++) {
|
|
369
|
-
if (args[i].startsWith("--")) {
|
|
370
|
-
const key = args[i].slice(2);
|
|
371
|
-
const next = args[i + 1];
|
|
372
|
-
if (next && !next.startsWith("--")) {
|
|
373
|
-
flags[key] = next;
|
|
374
|
-
i++;
|
|
375
|
-
}
|
|
376
|
-
else {
|
|
377
|
-
flags[key] = true;
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
else {
|
|
381
|
-
positional.push(args[i]);
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
return { flags, positional };
|
|
385
|
-
}
|
|
386
|
-
async function runScan() {
|
|
387
|
-
const args = process.argv.slice(2);
|
|
388
|
-
const { flags } = parseArgs(args);
|
|
389
|
-
const format = flags.format ?? "markdown";
|
|
390
|
-
const outputFile = flags.output ?? null;
|
|
391
|
-
let result;
|
|
392
|
-
if (format === "sarif") {
|
|
393
|
-
const { exportSarif } = await import("./tools/export-sarif.js");
|
|
394
|
-
result = exportSarif(process.cwd());
|
|
395
|
-
}
|
|
396
|
-
else {
|
|
397
|
-
const { scanStaged } = await import("./tools/scan-staged.js");
|
|
398
|
-
result = scanStaged(process.cwd(), format === "json" ? "json" : "markdown");
|
|
399
|
-
}
|
|
400
|
-
if (outputFile) {
|
|
401
|
-
writeFileSync(outputFile, result, "utf-8");
|
|
402
|
-
console.log(` [OK] Results written to ${outputFile}`);
|
|
403
|
-
}
|
|
404
|
-
else {
|
|
405
|
-
console.log(result);
|
|
406
|
-
}
|
|
407
|
-
if (format !== "sarif") {
|
|
408
|
-
const failOn = flags["fail-on"] ?? "high";
|
|
409
|
-
if (shouldFail(result, failOn))
|
|
410
|
-
process.exit(1);
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
async function runDirectoryScan(targetPath, flags) {
|
|
414
|
-
const { scanDirectory } = await import("./tools/scan-directory.js");
|
|
415
|
-
const { resolve } = await import("path");
|
|
416
|
-
const format = flags.format ?? "markdown";
|
|
417
|
-
const outputFile = flags.output ?? null;
|
|
418
|
-
const baselinePath = flags.baseline ?? null;
|
|
419
|
-
const saveBaseline = flags["save-baseline"] === true || typeof flags["save-baseline"] === "string";
|
|
420
|
-
const scanPath = resolve(targetPath);
|
|
421
|
-
let result;
|
|
422
|
-
if (format === "sarif") {
|
|
423
|
-
const { exportSarif } = await import("./tools/export-sarif.js");
|
|
424
|
-
result = exportSarif(scanPath);
|
|
425
|
-
}
|
|
426
|
-
else {
|
|
427
|
-
result = scanDirectory(scanPath, true, [], format === "json" ? "json" : "markdown", undefined, baselinePath ?? undefined);
|
|
428
|
-
}
|
|
429
|
-
if (outputFile) {
|
|
430
|
-
writeFileSync(outputFile, result, "utf-8");
|
|
431
|
-
console.log(` [OK] Results written to ${outputFile}`);
|
|
432
|
-
}
|
|
433
|
-
else {
|
|
434
|
-
console.log(result);
|
|
435
|
-
}
|
|
436
|
-
// Auto-save baseline for future diff comparisons
|
|
437
|
-
if (saveBaseline && format === "json") {
|
|
438
|
-
const baselineFile = typeof flags["save-baseline"] === "string"
|
|
439
|
-
? flags["save-baseline"]
|
|
440
|
-
: join(scanPath, ".guardvibe-baseline.json");
|
|
441
|
-
writeFileSync(baselineFile, result, "utf-8");
|
|
442
|
-
console.log(` [OK] Baseline saved to ${baselineFile}`);
|
|
443
|
-
}
|
|
444
|
-
if (format !== "sarif") {
|
|
445
|
-
const failOn = flags["fail-on"] ?? "critical";
|
|
446
|
-
if (shouldFail(result, failOn))
|
|
447
|
-
process.exit(1);
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
async function runDiffScan(base, flags) {
|
|
451
|
-
const { execFileSync } = await import("child_process");
|
|
452
|
-
const { resolve, extname, basename } = await import("path");
|
|
453
|
-
const { analyzeCode } = await import("./tools/check-code.js");
|
|
454
|
-
const { EXTENSION_MAP, CONFIG_FILE_MAP } = await import("./utils/constants.js");
|
|
455
|
-
const format = flags.format ?? "markdown";
|
|
456
|
-
const outputFile = flags.output ?? null;
|
|
457
|
-
const root = resolve(".");
|
|
458
|
-
let changedFiles;
|
|
459
|
-
try {
|
|
460
|
-
const output = execFileSync("git", ["diff", "--name-only", "--diff-filter=ACMR", base], { cwd: root, encoding: "utf-8" });
|
|
461
|
-
changedFiles = output.trim().split("\n").filter(Boolean);
|
|
462
|
-
}
|
|
463
|
-
catch {
|
|
464
|
-
console.error(" [ERR] Failed to get git diff. Ensure you're in a git repository.");
|
|
465
|
-
process.exit(1);
|
|
466
|
-
}
|
|
467
|
-
if (changedFiles.length === 0) {
|
|
468
|
-
console.log(" No changed files to scan.");
|
|
469
|
-
return;
|
|
470
|
-
}
|
|
471
|
-
const allFindings = [];
|
|
472
|
-
for (const relPath of changedFiles) {
|
|
473
|
-
const fullPath = resolve(root, relPath);
|
|
474
|
-
if (!existsSync(fullPath))
|
|
475
|
-
continue;
|
|
476
|
-
const ext = extname(relPath).toLowerCase();
|
|
477
|
-
let language = EXTENSION_MAP[ext];
|
|
478
|
-
if (!language && basename(relPath).startsWith("Dockerfile"))
|
|
479
|
-
language = "dockerfile";
|
|
480
|
-
if (!language)
|
|
481
|
-
language = CONFIG_FILE_MAP[basename(relPath)];
|
|
482
|
-
if (!language)
|
|
483
|
-
continue;
|
|
484
|
-
try {
|
|
485
|
-
const content = readFileSync(fullPath, "utf-8");
|
|
486
|
-
const findings = analyzeCode(content, language, undefined, fullPath, root);
|
|
487
|
-
for (const f of findings) {
|
|
488
|
-
allFindings.push({ file: relPath, severity: f.rule.severity, name: f.rule.name, id: f.rule.id, line: f.line, fix: f.rule.fix });
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
catch { /* skip */ }
|
|
492
|
-
}
|
|
493
|
-
let result;
|
|
494
|
-
if (format === "json") {
|
|
495
|
-
const critical = allFindings.filter(f => f.severity === "critical").length;
|
|
496
|
-
const high = allFindings.filter(f => f.severity === "high").length;
|
|
497
|
-
const medium = allFindings.filter(f => f.severity === "medium").length;
|
|
498
|
-
result = JSON.stringify({
|
|
499
|
-
summary: { total: allFindings.length, critical, high, medium, changedFiles: changedFiles.length, blocked: critical > 0 || high > 0 },
|
|
500
|
-
findings: allFindings,
|
|
501
|
-
});
|
|
502
|
-
}
|
|
503
|
-
else {
|
|
504
|
-
const lines = [`# GuardVibe Diff Report`, ``, `Base: ${base}`, `Changed files: ${changedFiles.length}`, `Issues: ${allFindings.length}`, ``];
|
|
505
|
-
if (allFindings.length === 0) {
|
|
506
|
-
lines.push(`All changed files passed security checks.`);
|
|
507
|
-
}
|
|
508
|
-
else {
|
|
509
|
-
const sev = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
|
|
510
|
-
allFindings.sort((a, b) => (sev[a.severity] ?? 99) - (sev[b.severity] ?? 99));
|
|
511
|
-
for (const f of allFindings) {
|
|
512
|
-
lines.push(`- [${f.severity.toUpperCase()}] **${f.name}** (${f.id}) in ${f.file}:${f.line}`);
|
|
513
|
-
lines.push(` Fix: ${f.fix}`);
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
result = lines.join("\n");
|
|
517
|
-
}
|
|
518
|
-
if (outputFile) {
|
|
519
|
-
writeFileSync(outputFile, result, "utf-8");
|
|
520
|
-
console.log(` [OK] Results written to ${outputFile}`);
|
|
521
|
-
}
|
|
522
|
-
else {
|
|
523
|
-
console.log(result);
|
|
524
|
-
}
|
|
525
|
-
const failOn = flags["fail-on"] ?? "critical";
|
|
526
|
-
if (failOn !== "none") {
|
|
527
|
-
const failLevels = {
|
|
528
|
-
low: ["critical", "high", "medium", "low"],
|
|
529
|
-
medium: ["critical", "high", "medium"],
|
|
530
|
-
high: ["critical", "high"],
|
|
531
|
-
critical: ["critical"],
|
|
532
|
-
};
|
|
533
|
-
const levels = failLevels[failOn] || failLevels.critical;
|
|
534
|
-
if (allFindings.some(f => levels.includes(f.severity)))
|
|
535
|
-
process.exit(1);
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
async function runFileCheck(filePath, flags) {
|
|
539
|
-
const { checkCode } = await import("./tools/check-code.js");
|
|
540
|
-
const { resolve, extname, basename } = await import("path");
|
|
541
|
-
const resolved = resolve(filePath);
|
|
542
|
-
if (!existsSync(resolved)) {
|
|
543
|
-
console.error(` [ERR] File not found: ${resolved}`);
|
|
544
|
-
process.exit(1);
|
|
545
|
-
}
|
|
546
|
-
const content = readFileSync(resolved, "utf-8");
|
|
547
|
-
const ext = extname(resolved).toLowerCase();
|
|
548
|
-
const extMap = {
|
|
549
|
-
".js": "javascript", ".jsx": "javascript", ".mjs": "javascript", ".cjs": "javascript",
|
|
550
|
-
".ts": "typescript", ".tsx": "typescript", ".mts": "typescript", ".cts": "typescript",
|
|
551
|
-
".py": "python", ".go": "go", ".html": "html", ".sql": "sql",
|
|
552
|
-
".sh": "shell", ".bash": "shell", ".yml": "yaml", ".yaml": "yaml",
|
|
553
|
-
".tf": "terraform", ".toml": "toml", ".json": "json",
|
|
554
|
-
};
|
|
555
|
-
let language = extMap[ext];
|
|
556
|
-
if (!language && basename(resolved).startsWith("Dockerfile"))
|
|
557
|
-
language = "dockerfile";
|
|
558
|
-
if (!language) {
|
|
559
|
-
console.error(` [ERR] Unsupported file type: ${ext}`);
|
|
560
|
-
process.exit(1);
|
|
561
|
-
}
|
|
562
|
-
const format = flags.format ?? "markdown";
|
|
563
|
-
const formatArg = format === "json" ? "json" : format === "buddy" ? "buddy" : "markdown";
|
|
564
|
-
const result = checkCode(content, language, undefined, resolved, undefined, formatArg);
|
|
565
|
-
const outputFile = flags.output ?? null;
|
|
566
|
-
if (outputFile) {
|
|
567
|
-
writeFileSync(outputFile, result, "utf-8");
|
|
568
|
-
console.log(` [OK] Results written to ${outputFile}`);
|
|
569
|
-
}
|
|
570
|
-
else {
|
|
571
|
-
console.log(result);
|
|
572
|
-
}
|
|
573
|
-
const failOn = flags["fail-on"] ?? "critical";
|
|
574
|
-
if (shouldFail(result, failOn))
|
|
575
|
-
process.exit(1);
|
|
576
|
-
}
|
|
577
|
-
// ── Main CLI ─────────────────────────────────────────────────────────
|
|
15
|
+
// ── Help ────────────────────────────────────────────────────────────
|
|
578
16
|
function printUsage() {
|
|
579
17
|
console.log(`
|
|
580
18
|
GuardVibe Security - CLI
|
|
@@ -583,6 +21,7 @@ function printUsage() {
|
|
|
583
21
|
npx guardvibe scan [path] Scan a directory for security issues
|
|
584
22
|
npx guardvibe diff [base] Scan only changed files since a git ref
|
|
585
23
|
npx guardvibe check <file> Scan a single file for security issues
|
|
24
|
+
npx guardvibe doctor [path] Run host security audit
|
|
586
25
|
npx guardvibe init <platform> Setup MCP server configuration
|
|
587
26
|
npx guardvibe hook install Install pre-commit security hook
|
|
588
27
|
npx guardvibe hook uninstall Remove pre-commit security hook
|
|
@@ -602,6 +41,12 @@ function printUsage() {
|
|
|
602
41
|
--version, -V Print version and exit
|
|
603
42
|
--help, -h Show this help message
|
|
604
43
|
|
|
44
|
+
Doctor Options:
|
|
45
|
+
--scope <scope> project (default) | host | full
|
|
46
|
+
project: only project config files
|
|
47
|
+
host: + shell profiles, global MCP configs
|
|
48
|
+
full: + home dir configs, npm global
|
|
49
|
+
|
|
605
50
|
MCP Platforms:
|
|
606
51
|
claude Claude Code (.claude.json + CLAUDE.md + hooks)
|
|
607
52
|
cursor Cursor (.cursor/mcp.json + .cursorrules)
|
|
@@ -615,16 +60,16 @@ function printUsage() {
|
|
|
615
60
|
Examples:
|
|
616
61
|
npx guardvibe scan .
|
|
617
62
|
npx guardvibe scan ./src --format json
|
|
618
|
-
npx guardvibe
|
|
619
|
-
npx guardvibe
|
|
620
|
-
npx guardvibe
|
|
63
|
+
npx guardvibe doctor
|
|
64
|
+
npx guardvibe doctor --scope host
|
|
65
|
+
npx guardvibe doctor --scope full --format json
|
|
621
66
|
npx guardvibe check src/app/api/route.ts
|
|
622
|
-
npx guardvibe check package.json
|
|
623
67
|
npx guardvibe init claude
|
|
624
68
|
npx guardvibe hook install
|
|
625
69
|
npx guardvibe ci github
|
|
626
70
|
`);
|
|
627
71
|
}
|
|
72
|
+
// ── Main dispatcher ─────────────────────────────────────────────────
|
|
628
73
|
async function main() {
|
|
629
74
|
const args = process.argv.slice(2);
|
|
630
75
|
if (args.includes("--version") || args.includes("-V")) {
|
|
@@ -642,91 +87,40 @@ async function main() {
|
|
|
642
87
|
process.exit(0);
|
|
643
88
|
}
|
|
644
89
|
else {
|
|
645
|
-
// Started by MCP client (Claude Code, Cursor, etc.) — launch MCP server
|
|
646
90
|
const { startMcpServer } = await import("./index.js");
|
|
647
91
|
await startMcpServer();
|
|
648
92
|
return;
|
|
649
93
|
}
|
|
650
94
|
}
|
|
651
95
|
const command = args[0];
|
|
96
|
+
const subArgs = args.slice(1);
|
|
652
97
|
if (command === "init") {
|
|
653
|
-
const
|
|
654
|
-
|
|
655
|
-
console.error(" Please specify a platform: claude, gemini, cursor, or all");
|
|
656
|
-
process.exit(1);
|
|
657
|
-
}
|
|
658
|
-
console.log(`\n GuardVibe Security Setup\n`);
|
|
659
|
-
if (platform === "all") {
|
|
660
|
-
for (const name of Object.keys(platforms)) {
|
|
661
|
-
setupPlatform(name);
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
else if (platforms[platform]) {
|
|
665
|
-
setupPlatform(platform);
|
|
666
|
-
}
|
|
667
|
-
else {
|
|
668
|
-
console.error(` Unknown platform: ${platform}`);
|
|
669
|
-
console.error(` Available: claude, gemini, cursor, all`);
|
|
670
|
-
process.exit(1);
|
|
671
|
-
}
|
|
672
|
-
console.log(`\n [OK] Ready! Start coding securely.\n`);
|
|
98
|
+
const { runInit } = await import("./cli/init.js");
|
|
99
|
+
runInit(subArgs);
|
|
673
100
|
}
|
|
674
101
|
else if (command === "hook") {
|
|
675
|
-
const
|
|
676
|
-
|
|
677
|
-
if (action === "install") {
|
|
678
|
-
installHook();
|
|
679
|
-
}
|
|
680
|
-
else if (action === "uninstall") {
|
|
681
|
-
uninstallHook();
|
|
682
|
-
}
|
|
683
|
-
else {
|
|
684
|
-
console.error(" Usage: npx guardvibe hook install|uninstall");
|
|
685
|
-
process.exit(1);
|
|
686
|
-
}
|
|
687
|
-
console.log();
|
|
102
|
+
const { runHook } = await import("./cli/hook.js");
|
|
103
|
+
runHook(subArgs);
|
|
688
104
|
}
|
|
689
105
|
else if (command === "scan") {
|
|
690
|
-
const
|
|
691
|
-
|
|
692
|
-
const targetPath = positional[0] ?? ".";
|
|
693
|
-
// If target is a file (not directory), auto-redirect to check mode
|
|
694
|
-
if (targetPath !== "." && existsSync(targetPath) && !statSync(targetPath).isDirectory()) {
|
|
695
|
-
console.log(` [INFO] "${targetPath}" is a file. Running: guardvibe check ${targetPath}\n`);
|
|
696
|
-
await runFileCheck(targetPath, flags);
|
|
697
|
-
}
|
|
698
|
-
else {
|
|
699
|
-
await runDirectoryScan(targetPath, flags);
|
|
700
|
-
}
|
|
106
|
+
const { handleScanCommand } = await import("./cli/scan.js");
|
|
107
|
+
await handleScanCommand(subArgs);
|
|
701
108
|
}
|
|
702
109
|
else if (command === "diff") {
|
|
703
|
-
const
|
|
704
|
-
|
|
705
|
-
const base = positional[0] ?? "main";
|
|
706
|
-
await runDiffScan(base, flags);
|
|
110
|
+
const { handleDiffCommand } = await import("./cli/scan.js");
|
|
111
|
+
await handleDiffCommand(subArgs);
|
|
707
112
|
}
|
|
708
113
|
else if (command === "check") {
|
|
709
|
-
const
|
|
710
|
-
|
|
711
|
-
const filePath = positional[0];
|
|
712
|
-
if (!filePath) {
|
|
713
|
-
console.error(" Please specify a file: npx guardvibe check <file>");
|
|
714
|
-
process.exit(1);
|
|
715
|
-
}
|
|
716
|
-
await runFileCheck(filePath, flags);
|
|
114
|
+
const { handleCheckCommand } = await import("./cli/scan.js");
|
|
115
|
+
await handleCheckCommand(subArgs);
|
|
717
116
|
}
|
|
718
117
|
else if (command === "ci") {
|
|
719
|
-
const
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
console.error(" Usage: npx guardvibe ci github");
|
|
726
|
-
console.error(" (more CI providers coming soon)");
|
|
727
|
-
process.exit(1);
|
|
728
|
-
}
|
|
729
|
-
console.log();
|
|
118
|
+
const { runCi } = await import("./cli/ci.js");
|
|
119
|
+
runCi(subArgs);
|
|
120
|
+
}
|
|
121
|
+
else if (command === "doctor") {
|
|
122
|
+
const { runDoctor } = await import("./cli/doctor.js");
|
|
123
|
+
await runDoctor(subArgs);
|
|
730
124
|
}
|
|
731
125
|
else {
|
|
732
126
|
console.error(` Unknown command: ${command}`);
|