guardvibe 2.4.5 → 2.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,296 @@
1
+ import { readFileSync, existsSync } from "fs";
2
+ import { join, resolve } from "path";
3
+ // Known suspicious patterns in hook commands
4
+ const NETWORK_CMDS = /\b(?:curl|wget|nc|ncat|netcat|fetch|http|telnet)\b/i;
5
+ const SHELL_CHAIN = /(?:\||\$\(|`[^`]+`|;\s*\w|&&\s*\w|>\s*\/)/;
6
+ const FILE_MODIFY = /\b(?:cp|mv|rm|chmod|chown|sed|tee|dd)\b/;
7
+ const EVAL_PIPE = /\|\s*(?:bash|sh|zsh|python|node|eval|exec)\b/;
8
+ const DANGEROUS_PATTERNS = /\b(?:eval|base64|decode|exec\s*\(|os\.system|subprocess)\b/i;
9
+ /**
10
+ * Scan MCP configuration files for security issues.
11
+ * Standalone tool + called by doctor.
12
+ */
13
+ export function auditMcpConfig(projectPath, doctorConfig) {
14
+ const findings = [];
15
+ const scannedFiles = [];
16
+ const skippedFiles = [];
17
+ const root = resolve(projectPath);
18
+ const configFiles = [
19
+ { path: join(root, ".claude.json"), host: "Claude" },
20
+ { path: join(root, ".claude", "settings.json"), host: "Claude" },
21
+ { path: join(root, ".cursor", "mcp.json"), host: "Cursor" },
22
+ { path: join(root, ".vscode", "mcp.json"), host: "VS Code" },
23
+ { path: join(root, ".vscode", "settings.json"), host: "VS Code" },
24
+ ];
25
+ const trustedServers = new Set(doctorConfig?.trustedServers ?? []);
26
+ const ignorePaths = new Set(doctorConfig?.ignorePaths ?? []);
27
+ for (const { path: configPath, host } of configFiles) {
28
+ if (ignorePaths.has(configPath) || ignorePaths.has(configPath.replace(root + "/", ""))) {
29
+ skippedFiles.push(configPath);
30
+ continue;
31
+ }
32
+ if (!existsSync(configPath)) {
33
+ skippedFiles.push(configPath);
34
+ continue;
35
+ }
36
+ scannedFiles.push(configPath);
37
+ let content;
38
+ try {
39
+ content = readFileSync(configPath, "utf-8");
40
+ }
41
+ catch {
42
+ skippedFiles.push(configPath);
43
+ continue;
44
+ }
45
+ let parsed;
46
+ try {
47
+ parsed = JSON.parse(content);
48
+ }
49
+ catch {
50
+ findings.push({
51
+ ruleId: "VG-HOST-001",
52
+ severity: "info",
53
+ trustState: "unknown",
54
+ verdict: "observed",
55
+ confidence: "high",
56
+ source: "core",
57
+ file: configPath,
58
+ description: `${host} config file contains invalid JSON`,
59
+ remediation: "Fix JSON syntax errors in the configuration file.",
60
+ });
61
+ continue;
62
+ }
63
+ // Scan hooks (Claude settings.json)
64
+ if (parsed.hooks && typeof parsed.hooks === "object") {
65
+ scanHooks(parsed, configPath, findings);
66
+ }
67
+ // Scan allowedTools
68
+ if (Array.isArray(parsed.allowedTools)) {
69
+ scanAllowedTools(parsed.allowedTools, configPath, findings);
70
+ }
71
+ // Scan MCP server entries
72
+ const servers = parsed.mcpServers ?? parsed.servers;
73
+ if (servers && typeof servers === "object") {
74
+ scanMcpServers(servers, configPath, host, trustedServers, findings);
75
+ }
76
+ }
77
+ return { findings, scannedFiles, skippedFiles };
78
+ }
79
+ function scanHooks(settings, file, findings) {
80
+ if (!settings.hooks)
81
+ return;
82
+ for (const [hookName, hooks] of Object.entries(settings.hooks)) {
83
+ if (!Array.isArray(hooks))
84
+ continue;
85
+ for (const hook of hooks) {
86
+ const cmd = hook.command;
87
+ if (!cmd)
88
+ continue;
89
+ // VG884: Shell metacharacters
90
+ if (SHELL_CHAIN.test(cmd)) {
91
+ findings.push({
92
+ ruleId: "VG884",
93
+ severity: "critical",
94
+ trustState: "suspicious",
95
+ verdict: "exploitable",
96
+ confidence: "high",
97
+ source: "core",
98
+ file,
99
+ description: `${hookName} hook contains shell metacharacters: potential command injection (CVE-2025-59536)`,
100
+ remediation: "Remove shell metacharacters (|, ;, &&, $()) from hook commands. Use simple, direct commands.",
101
+ patchPreview: `"${hookName}": [{ "command": "<simple-command-without-pipes>" }]`,
102
+ });
103
+ }
104
+ // VG890: Network requests
105
+ if (NETWORK_CMDS.test(cmd)) {
106
+ findings.push({
107
+ ruleId: "VG890",
108
+ severity: "critical",
109
+ trustState: "suspicious",
110
+ verdict: "exploitable",
111
+ confidence: "high",
112
+ source: "core",
113
+ file,
114
+ description: `${hookName} hook executes network requests — potential data exfiltration`,
115
+ remediation: "Remove network request commands (curl, wget, nc) from hooks. Hooks should only perform local operations.",
116
+ });
117
+ }
118
+ // VG891: Pipe to interpreter
119
+ if (EVAL_PIPE.test(cmd)) {
120
+ findings.push({
121
+ ruleId: "VG891",
122
+ severity: "high",
123
+ trustState: "suspicious",
124
+ verdict: "risky",
125
+ confidence: "high",
126
+ source: "core",
127
+ file,
128
+ description: `${hookName} hook pipes output to interpreter (bash/python/node) — code execution risk`,
129
+ remediation: "Remove pipe chains to interpreters. Process output in a dedicated script if needed.",
130
+ });
131
+ }
132
+ // VG895: PostToolUse file modifications
133
+ if (hookName === "PostToolUse" && FILE_MODIFY.test(cmd)) {
134
+ findings.push({
135
+ ruleId: "VG895",
136
+ severity: "high",
137
+ trustState: "suspicious",
138
+ verdict: "risky",
139
+ confidence: "high",
140
+ source: "core",
141
+ file,
142
+ description: `PostToolUse hook modifies files silently — potential backdoor or code tampering`,
143
+ remediation: "Remove file-modifying commands from PostToolUse hooks. Hooks should only observe and report.",
144
+ });
145
+ }
146
+ // Generic dangerous patterns
147
+ if (DANGEROUS_PATTERNS.test(cmd) && !NETWORK_CMDS.test(cmd)) {
148
+ findings.push({
149
+ ruleId: "VG884",
150
+ severity: "high",
151
+ trustState: "suspicious",
152
+ verdict: "risky",
153
+ confidence: "medium",
154
+ source: "core",
155
+ file,
156
+ description: `${hookName} hook contains dangerous pattern (eval/base64/exec) — potential code execution`,
157
+ remediation: "Remove eval, base64 decode, and exec patterns from hook commands.",
158
+ });
159
+ }
160
+ }
161
+ }
162
+ }
163
+ function scanAllowedTools(tools, file, findings) {
164
+ for (const tool of tools) {
165
+ if (tool === "*") {
166
+ findings.push({
167
+ ruleId: "VG885",
168
+ severity: "medium",
169
+ trustState: "unknown",
170
+ verdict: "risky",
171
+ confidence: "high",
172
+ source: "core",
173
+ file,
174
+ description: 'allowedTools contains wildcard "*" — all tools are accessible without restriction',
175
+ remediation: "Replace wildcard tool access with explicit tool names that the MCP server actually needs.",
176
+ patchPreview: '"allowedTools": ["read_file", "list_directory"]',
177
+ });
178
+ }
179
+ else if (/\*/.test(tool) && tool !== "*") {
180
+ // Broad wildcards like mcp__*, edit*, etc.
181
+ const prefix = tool.replace(/\*/g, "");
182
+ if (prefix.length < 10) {
183
+ findings.push({
184
+ ruleId: "VG893",
185
+ severity: "medium",
186
+ trustState: "unknown",
187
+ verdict: "risky",
188
+ confidence: "medium",
189
+ source: "core",
190
+ file,
191
+ description: `allowedTools contains broad wildcard "${tool}" — grants more access than intended`,
192
+ remediation: "Replace broad wildcards with specific tool names. Use exact match patterns.",
193
+ });
194
+ }
195
+ }
196
+ }
197
+ }
198
+ function scanMcpServers(servers, file, host, trustedServers, findings) {
199
+ for (const [name, server] of Object.entries(servers)) {
200
+ // Skip trusted servers
201
+ if (isTrusted(name, trustedServers))
202
+ continue;
203
+ // VG892: file:// references
204
+ const url = server.url ?? "";
205
+ if (/^file:\/\//i.test(url)) {
206
+ findings.push({
207
+ ruleId: "VG892",
208
+ severity: "high",
209
+ trustState: "suspicious",
210
+ verdict: "risky",
211
+ confidence: "high",
212
+ source: "core",
213
+ file,
214
+ description: `MCP server "${name}" uses file:// URL — potential local file access`,
215
+ remediation: "Use npm packages or HTTPS URLs for MCP servers. Avoid file:// references.",
216
+ });
217
+ }
218
+ // Non-HTTPS URL
219
+ if (url && /^http:\/\//i.test(url)) {
220
+ findings.push({
221
+ ruleId: "VG892",
222
+ severity: "medium",
223
+ trustState: "unknown",
224
+ verdict: "risky",
225
+ confidence: "medium",
226
+ source: "core",
227
+ file,
228
+ description: `MCP server "${name}" uses HTTP (not HTTPS) — traffic is unencrypted`,
229
+ remediation: "Use HTTPS for MCP server connections to prevent traffic interception.",
230
+ });
231
+ }
232
+ // Check command for suspicious patterns
233
+ const cmd = server.command ?? "";
234
+ const argsStr = (server.args ?? []).join(" ");
235
+ const fullCmd = `${cmd} ${argsStr}`;
236
+ if (NETWORK_CMDS.test(fullCmd) && SHELL_CHAIN.test(fullCmd)) {
237
+ findings.push({
238
+ ruleId: "VG890",
239
+ severity: "high",
240
+ trustState: "suspicious",
241
+ verdict: "risky",
242
+ confidence: "medium",
243
+ source: "core",
244
+ file,
245
+ description: `MCP server "${name}" command contains network requests with shell chaining`,
246
+ remediation: "Review the MCP server command for suspicious network activity.",
247
+ });
248
+ }
249
+ // Check for env overrides in server config
250
+ if (server.env) {
251
+ for (const [key, val] of Object.entries(server.env)) {
252
+ if (/ANTHROPIC_BASE_URL/i.test(key) && !/api\.anthropic\.com/.test(val)) {
253
+ findings.push({
254
+ ruleId: "VG882",
255
+ severity: "high",
256
+ trustState: "suspicious",
257
+ verdict: "risky",
258
+ confidence: "medium",
259
+ source: "core",
260
+ file,
261
+ description: `MCP server "${name}" overrides ANTHROPIC_BASE_URL — API traffic redirection`,
262
+ remediation: "Remove ANTHROPIC_BASE_URL override or add the URL to trustedBaseUrls in .guardviberc.",
263
+ });
264
+ }
265
+ }
266
+ }
267
+ // VG894: Sensitive path access
268
+ const sensitivePaths = /(?:\.ssh|\.gnupg|\.aws|\.kube|\/etc(?:\/|\b)|\.config\/gcloud)/i;
269
+ if (sensitivePaths.test(fullCmd) || sensitivePaths.test(JSON.stringify(server))) {
270
+ findings.push({
271
+ ruleId: "VG894",
272
+ severity: "high",
273
+ trustState: "suspicious",
274
+ verdict: "risky",
275
+ confidence: "high",
276
+ source: "core",
277
+ file,
278
+ description: `MCP server "${name}" references security-sensitive paths (.ssh, .aws, .gnupg, /etc)`,
279
+ remediation: "Remove security-sensitive paths from MCP server configuration. Limit access to project directories.",
280
+ });
281
+ }
282
+ }
283
+ }
284
+ function isTrusted(name, trustedServers) {
285
+ if (trustedServers.has(name))
286
+ return true;
287
+ // Check glob patterns like @anthropic/*
288
+ for (const pattern of trustedServers) {
289
+ if (pattern.endsWith("/*")) {
290
+ const prefix = pattern.slice(0, -2);
291
+ if (name.startsWith(prefix))
292
+ return true;
293
+ }
294
+ }
295
+ return false;
296
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Cross-file taint analysis — tracks user input flowing across module boundaries.
3
+ * Resolves imports/exports, builds a module graph, and propagates taint between files.
4
+ */
5
+ import { type TaintFinding } from "./taint-analysis.js";
6
+ export interface FileEntry {
7
+ path: string;
8
+ content: string;
9
+ }
10
+ export interface CrossFileTaintFinding {
11
+ source: {
12
+ file: string;
13
+ type: string;
14
+ line: number;
15
+ variable: string;
16
+ };
17
+ sink: {
18
+ file: string;
19
+ type: string;
20
+ line: number;
21
+ code: string;
22
+ };
23
+ chain: string[];
24
+ severity: "critical" | "high" | "medium";
25
+ description: string;
26
+ fix: string;
27
+ }
28
+ interface ImportInfo {
29
+ /** The file that contains the import statement */
30
+ importer: string;
31
+ /** Resolved path of the module being imported */
32
+ source: string;
33
+ /** Named imports: local name -> exported name */
34
+ names: Map<string, string>;
35
+ /** Default import name, if any */
36
+ defaultName?: string;
37
+ /** Namespace import name (import * as X), if any */
38
+ namespaceName?: string;
39
+ /** Line number of the import statement */
40
+ line: number;
41
+ }
42
+ interface ExportInfo {
43
+ /** The file that exports */
44
+ file: string;
45
+ /** Exported name -> local name */
46
+ names: Map<string, string>;
47
+ /** Has default export */
48
+ hasDefault: boolean;
49
+ /** Default export local name (function/class name or "default") */
50
+ defaultLocal?: string;
51
+ }
52
+ interface FunctionSignature {
53
+ file: string;
54
+ name: string;
55
+ params: string[];
56
+ startLine: number;
57
+ endLine: number;
58
+ body: string;
59
+ }
60
+ interface TaintedExport {
61
+ file: string;
62
+ exportName: string;
63
+ /** Which parameter indices receive taint and flow to sinks */
64
+ taintedParams: Map<number, {
65
+ sinkType: string;
66
+ sinkLine: number;
67
+ sinkCode: string;
68
+ }>;
69
+ }
70
+ declare function normalizePath(from: string, importPath: string): string;
71
+ declare function stripExtension(filePath: string): string;
72
+ declare function parseImports(file: string, content: string): ImportInfo[];
73
+ declare function parseExports(file: string, content: string): ExportInfo;
74
+ declare function extractFunctions(file: string, content: string): FunctionSignature[];
75
+ declare function findTaintedExports(files: FileEntry[]): TaintedExport[];
76
+ export declare function analyzeCrossFileTaint(files: FileEntry[]): {
77
+ crossFileFindings: CrossFileTaintFinding[];
78
+ perFileFindings: Map<string, TaintFinding[]>;
79
+ };
80
+ export declare function formatCrossFileTaintFindings(crossFileFindings: CrossFileTaintFinding[], perFileFindings: Map<string, TaintFinding[]>, format: "markdown" | "json"): string;
81
+ export { parseImports, parseExports, extractFunctions, findTaintedExports, normalizePath, stripExtension };