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.
Files changed (39) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +43 -14
  3. package/build/cli/args.d.ts +15 -0
  4. package/build/cli/args.js +78 -0
  5. package/build/cli/ci.d.ts +5 -0
  6. package/build/cli/ci.js +67 -0
  7. package/build/cli/doctor.d.ts +5 -0
  8. package/build/cli/doctor.js +35 -0
  9. package/build/cli/hook.d.ts +5 -0
  10. package/build/cli/hook.js +90 -0
  11. package/build/cli/init.d.ts +5 -0
  12. package/build/cli/init.js +214 -0
  13. package/build/cli/remediation.d.ts +14 -0
  14. package/build/cli/remediation.js +91 -0
  15. package/build/cli/scan.d.ts +11 -0
  16. package/build/cli/scan.js +227 -0
  17. package/build/cli.js +33 -639
  18. package/build/data/rules/ai-host-security.d.ts +2 -0
  19. package/build/data/rules/ai-host-security.js +128 -0
  20. package/build/data/rules/ai-tool-runtime.d.ts +2 -0
  21. package/build/data/rules/ai-tool-runtime.js +54 -0
  22. package/build/data/rules/index.js +4 -0
  23. package/build/index.js +36 -0
  24. package/build/server/register.d.ts +7 -0
  25. package/build/server/register.js +7 -0
  26. package/build/server/types.d.ts +40 -0
  27. package/build/server/types.js +138 -0
  28. package/build/tools/audit-mcp-config.d.ts +10 -0
  29. package/build/tools/audit-mcp-config.js +296 -0
  30. package/build/tools/check-code.js +3 -0
  31. package/build/tools/doctor.d.ts +14 -0
  32. package/build/tools/doctor.js +123 -0
  33. package/build/tools/scan-directory.js +2 -0
  34. package/build/tools/scan-host-config.d.ts +10 -0
  35. package/build/tools/scan-host-config.js +181 -0
  36. package/build/tools/scan-staged.js +2 -0
  37. package/build/utils/banner.d.ts +36 -0
  38. package/build/utils/banner.js +66 -0
  39. package/package.json +2 -3
@@ -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
+ }
@@ -2,6 +2,7 @@ import { basename } from "path";
2
2
  import { owaspRules } from "../data/rules/index.js";
3
3
  import { loadConfig } from "../utils/config.js";
4
4
  import { loadIgnoreFile, isIgnored } from "../utils/ignore.js";
5
+ import { securityBanner } from "../utils/banner.js";
5
6
  function parseSuppressionsFromCode(lines) {
6
7
  const suppressions = [];
7
8
  const pattern = /(?:\/\/|#|<!--)\s*guardvibe-ignore(?:-next-line)?\s*(VG\d+)?\s*(?:-->)?/i;
@@ -441,6 +442,7 @@ function formatCleanReport(language, framework) {
441
442
  ``,
442
443
  `Tips for ${language}${ctx}:`,
443
444
  ...tips.map(t => `- ${t}`),
445
+ securityBanner({ total: 0, critical: 0, high: 0, medium: 0 }),
444
446
  ].join("\n");
445
447
  }
446
448
  function getLanguageTips(language, framework) {
@@ -549,6 +551,7 @@ function formatReport(findings, language, framework) {
549
551
  }
550
552
  }
551
553
  }
554
+ lines.push(securityBanner({ total: allFindings.length, critical: criticalCount, high: highCount, medium: mediumCount }));
552
555
  return lines.join("\n");
553
556
  }
554
557
  // ─── Buddy Format ────────────────────────────────────────────────
@@ -0,0 +1,14 @@
1
+ import type { DoctorScope } from "../server/types.js";
2
+ /**
3
+ * guardvibe_doctor — Unified host hardening scanner
4
+ *
5
+ * Orchestrates multiple analyzers to provide a comprehensive
6
+ * security assessment of AI coding host configuration.
7
+ *
8
+ * Analyzers:
9
+ * 1. MCP Config (audit_mcp_config) — hooks, servers, tool access
10
+ * 2. Host Environment (scan_host_config) — base URL hijack, env sniffing
11
+ * 3. Permissions (inline) — allowedTools wildcards, sensitive paths
12
+ * 4. File Transport (inline) — file:// references, path traversal
13
+ */
14
+ export declare function doctor(projectPath: string, scope?: DoctorScope, format?: "markdown" | "json"): string;
@@ -0,0 +1,123 @@
1
+ import { readFileSync, existsSync } from "fs";
2
+ import { join, resolve } from "path";
3
+ import { formatHostFindings, redactSecrets } from "../server/types.js";
4
+ import { auditMcpConfig } from "./audit-mcp-config.js";
5
+ import { scanHostConfig } from "./scan-host-config.js";
6
+ import { enrichAllRemediations } from "../cli/remediation.js";
7
+ /**
8
+ * guardvibe_doctor — Unified host hardening scanner
9
+ *
10
+ * Orchestrates multiple analyzers to provide a comprehensive
11
+ * security assessment of AI coding host configuration.
12
+ *
13
+ * Analyzers:
14
+ * 1. MCP Config (audit_mcp_config) — hooks, servers, tool access
15
+ * 2. Host Environment (scan_host_config) — base URL hijack, env sniffing
16
+ * 3. Permissions (inline) — allowedTools wildcards, sensitive paths
17
+ * 4. File Transport (inline) — file:// references, path traversal
18
+ */
19
+ export function doctor(projectPath, scope = "project", format = "markdown") {
20
+ const root = resolve(projectPath);
21
+ const doctorConfig = loadDoctorConfig(root);
22
+ const allFindings = [];
23
+ const allScanned = [];
24
+ const allSkipped = [];
25
+ // ── Analyzer 1: MCP Config ──────────────────────────────────────
26
+ const mcpResult = auditMcpConfig(root, doctorConfig);
27
+ allFindings.push(...mcpResult.findings);
28
+ allScanned.push(...mcpResult.scannedFiles);
29
+ allSkipped.push(...mcpResult.skippedFiles);
30
+ // ── Analyzer 2: Host Environment ────────────────────────────────
31
+ const hostResult = scanHostConfig(root, scope, doctorConfig);
32
+ allFindings.push(...hostResult.findings);
33
+ allScanned.push(...hostResult.scannedFiles);
34
+ allSkipped.push(...hostResult.skippedFiles);
35
+ // ── Analyzer 3: Permissions (inline scan) ───────────────────────
36
+ scanPermissions(root, doctorConfig, allFindings, allScanned, allSkipped);
37
+ // ── Host-Specific Remediation Enrichment ─────────────────────────
38
+ enrichAllRemediations(allFindings);
39
+ // ── Output ──────────────────────────────────────────────────────
40
+ const title = `GuardVibe Doctor — Host Security Audit (scope: ${scope})`;
41
+ const output = formatHostFindings(allFindings, allScanned, allSkipped, format, title);
42
+ return redactSecrets(output);
43
+ }
44
+ /**
45
+ * Load doctor-specific config from .guardviberc
46
+ */
47
+ function loadDoctorConfig(root) {
48
+ const configPath = findConfigFileUp(root, ".guardviberc");
49
+ if (!configPath)
50
+ return {};
51
+ try {
52
+ const content = readFileSync(configPath, "utf-8");
53
+ const parsed = JSON.parse(content);
54
+ return parsed.doctor ?? {};
55
+ }
56
+ catch {
57
+ return {};
58
+ }
59
+ }
60
+ function findConfigFileUp(startDir, filename) {
61
+ let current = startDir;
62
+ const fsRoot = resolve("/");
63
+ while (true) {
64
+ const candidate = join(current, filename);
65
+ if (existsSync(candidate))
66
+ return candidate;
67
+ const parent = resolve(current, "..");
68
+ if (parent === current || current === fsRoot)
69
+ break;
70
+ current = parent;
71
+ }
72
+ return null;
73
+ }
74
+ /**
75
+ * Inline permissions analyzer — checks for overly permissive
76
+ * configurations that don't fit in other analyzers.
77
+ */
78
+ function scanPermissions(root, _doctorConfig, findings, scannedFiles, _skippedFiles) {
79
+ // Check .claude.json for permissive patterns
80
+ const claudeJson = join(root, ".claude.json");
81
+ if (existsSync(claudeJson)) {
82
+ if (!scannedFiles.includes(claudeJson))
83
+ scannedFiles.push(claudeJson);
84
+ try {
85
+ const content = readFileSync(claudeJson, "utf-8");
86
+ const parsed = JSON.parse(content);
87
+ // Check for allowedTools with dangerous patterns
88
+ if (Array.isArray(parsed.permissions?.allow)) {
89
+ for (const perm of parsed.permissions.allow) {
90
+ if (typeof perm === "string" && /^(?:Bash|Edit|Write)\(.*\*.*\)$/i.test(perm)) {
91
+ findings.push({
92
+ ruleId: "VG893",
93
+ severity: "medium",
94
+ trustState: "unknown",
95
+ verdict: "risky",
96
+ confidence: "medium",
97
+ source: "core",
98
+ file: claudeJson,
99
+ description: `Broad permission pattern "${perm}" — may grant unintended access`,
100
+ remediation: "Replace broad wildcard permissions with specific, scoped patterns.",
101
+ });
102
+ }
103
+ }
104
+ }
105
+ // Check for deny list being empty when allow list is broad
106
+ if (Array.isArray(parsed.permissions?.allow) && parsed.permissions.allow.length > 10
107
+ && (!parsed.permissions?.deny || parsed.permissions.deny.length === 0)) {
108
+ findings.push({
109
+ ruleId: "VG885",
110
+ severity: "low",
111
+ trustState: "unknown",
112
+ verdict: "observed",
113
+ confidence: "low",
114
+ source: "core",
115
+ file: claudeJson,
116
+ description: "Large allow list with no deny list — consider adding explicit denials for sensitive operations",
117
+ remediation: "Add a deny list to explicitly block dangerous operations (e.g., rm -rf, git push --force).",
118
+ });
119
+ }
120
+ }
121
+ catch { /* invalid JSON already caught by MCP config scanner */ }
122
+ }
123
+ }
@@ -6,6 +6,7 @@ import { analyzeCode } from "./check-code.js";
6
6
  import { loadConfig } from "../utils/config.js";
7
7
  import { DEFAULT_EXCLUDES, EXTENSION_MAP, CONFIG_FILE_MAP } from "../utils/constants.js";
8
8
  import { walkDirectory } from "../utils/walk-directory.js";
9
+ import { securityBanner } from "../utils/banner.js";
9
10
  const require = createRequire(import.meta.url);
10
11
  const pkg = require("../../package.json");
11
12
  // GuardVibe version — used in scan metadata
@@ -240,5 +241,6 @@ export function scanDirectory(path, recursive = true, exclude = [], format = "ma
240
241
  for (const s of skippedFiles)
241
242
  lines.push(`- ${s}`);
242
243
  }
244
+ lines.push(securityBanner({ total: totalIssues, critical: totalCritical, high: totalHigh, medium: totalMedium, score, grade, filesScanned: metadata.filesScanned }));
243
245
  return lines.join("\n");
244
246
  }
@@ -0,0 +1,10 @@
1
+ import type { HostFinding, DoctorConfig, DoctorScope } from "../server/types.js";
2
+ /**
3
+ * Scan host environment configuration for security issues.
4
+ * Checks .env files, shell profiles, and environment variables.
5
+ */
6
+ export declare function scanHostConfig(projectPath: string, scope?: DoctorScope, doctorConfig?: DoctorConfig): {
7
+ findings: HostFinding[];
8
+ scannedFiles: string[];
9
+ skippedFiles: string[];
10
+ };
@@ -0,0 +1,181 @@
1
+ import { readFileSync, existsSync } from "fs";
2
+ import { join, resolve } from "path";
3
+ import { homedir } from "os";
4
+ // Known legitimate base URLs
5
+ const ANTHROPIC_HOSTS = ["api.anthropic.com"];
6
+ const OPENAI_HOSTS = ["api.openai.com"];
7
+ const BASE_URL_PATTERN = /(?:ANTHROPIC_BASE_URL|OPENAI_BASE_URL|ANTHROPIC_API_BASE|OPENAI_API_BASE)\s*=\s*['"]?(https?:\/\/[^\s'"#]+)/gi;
8
+ const API_KEY_EXPORT = /export\s+(?:ANTHROPIC_API_KEY|OPENAI_API_KEY|ANTHROPIC_KEY|OPENAI_KEY)\s*=\s*['"]?([^\s'"]+)/gi;
9
+ const ENV_SNIFF = /(?:echo|cat|printenv|env\s|set\s)[\s|]*(?:\$(?:ANTHROPIC|OPENAI|CLAUDE|API)_\w+|\$\{(?:ANTHROPIC|OPENAI|CLAUDE|API)_\w+\})/gi;
10
+ /**
11
+ * Scan host environment configuration for security issues.
12
+ * Checks .env files, shell profiles, and environment variables.
13
+ */
14
+ export function scanHostConfig(projectPath, scope = "project", doctorConfig) {
15
+ const findings = [];
16
+ const scannedFiles = [];
17
+ const skippedFiles = [];
18
+ const root = resolve(projectPath);
19
+ const home = homedir();
20
+ const trustedBaseUrls = new Set(doctorConfig?.trustedBaseUrls ?? []);
21
+ const ignorePaths = new Set(doctorConfig?.ignorePaths ?? []);
22
+ // Project-scope .env files
23
+ const envFiles = [
24
+ join(root, ".env"),
25
+ join(root, ".env.local"),
26
+ join(root, ".env.production"),
27
+ join(root, ".env.development"),
28
+ ];
29
+ for (const envFile of envFiles) {
30
+ const relPath = envFile.replace(root + "/", "");
31
+ if (ignorePaths.has(relPath) || ignorePaths.has(envFile)) {
32
+ skippedFiles.push(envFile);
33
+ continue;
34
+ }
35
+ scanEnvFile(envFile, findings, scannedFiles, skippedFiles, trustedBaseUrls);
36
+ }
37
+ // Host-scope: shell profiles
38
+ if (scope === "host" || scope === "full") {
39
+ const shellProfiles = [
40
+ join(home, ".bashrc"),
41
+ join(home, ".zshrc"),
42
+ join(home, ".profile"),
43
+ join(home, ".bash_profile"),
44
+ join(home, ".zprofile"),
45
+ ];
46
+ for (const profile of shellProfiles) {
47
+ scanShellProfile(profile, findings, scannedFiles, skippedFiles, trustedBaseUrls);
48
+ }
49
+ }
50
+ // Host-scope: global AI host configs
51
+ if (scope === "host" || scope === "full") {
52
+ const globalConfigs = [
53
+ { path: join(home, ".gemini", "settings.json"), host: "Gemini" },
54
+ { path: join(home, ".codeium", "windsurf", "mcp_config.json"), host: "Windsurf" },
55
+ ];
56
+ for (const { path: configPath } of globalConfigs) {
57
+ if (!existsSync(configPath)) {
58
+ skippedFiles.push(configPath);
59
+ continue;
60
+ }
61
+ scannedFiles.push(configPath);
62
+ try {
63
+ const content = readFileSync(configPath, "utf-8");
64
+ scanContentForBaseUrls(content, configPath, findings, trustedBaseUrls);
65
+ }
66
+ catch {
67
+ skippedFiles.push(configPath);
68
+ }
69
+ }
70
+ }
71
+ return { findings, scannedFiles, skippedFiles };
72
+ }
73
+ function scanEnvFile(filePath, findings, scannedFiles, skippedFiles, trustedBaseUrls) {
74
+ if (!existsSync(filePath)) {
75
+ skippedFiles.push(filePath);
76
+ return;
77
+ }
78
+ scannedFiles.push(filePath);
79
+ let content;
80
+ try {
81
+ content = readFileSync(filePath, "utf-8");
82
+ }
83
+ catch {
84
+ skippedFiles.push(filePath);
85
+ return;
86
+ }
87
+ scanContentForBaseUrls(content, filePath, findings, trustedBaseUrls);
88
+ }
89
+ function scanShellProfile(filePath, findings, scannedFiles, skippedFiles, trustedBaseUrls) {
90
+ if (!existsSync(filePath)) {
91
+ skippedFiles.push(filePath);
92
+ return;
93
+ }
94
+ scannedFiles.push(filePath);
95
+ let content;
96
+ try {
97
+ content = readFileSync(filePath, "utf-8");
98
+ }
99
+ catch {
100
+ skippedFiles.push(filePath);
101
+ return;
102
+ }
103
+ // Check for base URL overrides
104
+ scanContentForBaseUrls(content, filePath, findings, trustedBaseUrls);
105
+ // Check for env variable sniffing patterns
106
+ ENV_SNIFF.lastIndex = 0;
107
+ let match;
108
+ while ((match = ENV_SNIFF.exec(content)) !== null) {
109
+ const lineNum = content.slice(0, match.index).split("\n").length;
110
+ findings.push({
111
+ ruleId: "VG882",
112
+ severity: "medium",
113
+ trustState: "suspicious",
114
+ verdict: "risky",
115
+ confidence: "low",
116
+ source: "core",
117
+ file: filePath,
118
+ line: lineNum,
119
+ description: "Shell profile reads/outputs AI API environment variables — potential credential sniffing",
120
+ remediation: "Review why shell profile accesses AI API keys. Remove if not intentional.",
121
+ });
122
+ }
123
+ // Check for API key exports (hardcoded in profile)
124
+ API_KEY_EXPORT.lastIndex = 0;
125
+ while ((match = API_KEY_EXPORT.exec(content)) !== null) {
126
+ const lineNum = content.slice(0, match.index).split("\n").length;
127
+ findings.push({
128
+ ruleId: "VG882",
129
+ severity: "high",
130
+ trustState: "suspicious",
131
+ verdict: "risky",
132
+ confidence: "high",
133
+ source: "core",
134
+ file: filePath,
135
+ line: lineNum,
136
+ description: "API key exported in shell profile — key is visible in process environment and shell history",
137
+ remediation: "Move API keys to a secure secrets manager or .env file (not tracked by git). Remove from shell profile.",
138
+ });
139
+ }
140
+ }
141
+ function scanContentForBaseUrls(content, file, findings, trustedBaseUrls) {
142
+ BASE_URL_PATTERN.lastIndex = 0;
143
+ let match;
144
+ while ((match = BASE_URL_PATTERN.exec(content)) !== null) {
145
+ const url = match[1];
146
+ const lineNum = content.slice(0, match.index).split("\n").length;
147
+ // Check if trusted
148
+ if (trustedBaseUrls.has(url)) {
149
+ continue;
150
+ }
151
+ // Determine which provider
152
+ const envVar = match[0].split("=")[0].trim();
153
+ const isAnthropic = /ANTHROPIC/i.test(envVar);
154
+ const isOpenai = /OPENAI/i.test(envVar);
155
+ let hostname;
156
+ try {
157
+ hostname = new URL(url).hostname;
158
+ }
159
+ catch {
160
+ hostname = url;
161
+ }
162
+ const legitimateHosts = isAnthropic ? ANTHROPIC_HOSTS : isOpenai ? OPENAI_HOSTS : [];
163
+ const isLegitimate = legitimateHosts.some(h => hostname === h || hostname.endsWith(`.${h}`));
164
+ if (isLegitimate)
165
+ continue;
166
+ const isLocalhost = /^(?:localhost|127\.0\.0\.1|0\.0\.0\.0|::1)/.test(hostname);
167
+ findings.push({
168
+ ruleId: isAnthropic ? "VG882" : "VG883",
169
+ severity: isLocalhost ? "medium" : "high",
170
+ trustState: isLocalhost ? "unknown" : "suspicious",
171
+ verdict: isLocalhost ? "observed" : "risky",
172
+ confidence: isLocalhost ? "medium" : "medium",
173
+ source: "core",
174
+ file,
175
+ line: lineNum,
176
+ description: `${envVar} set to non-official domain (${hostname}) — API traffic redirection${isAnthropic ? " (CVE-2026-21852)" : ""}`,
177
+ remediation: `Remove the ${envVar} override, or add "${url}" to trustedBaseUrls in .guardviberc if it's a legitimate corporate proxy.`,
178
+ patchPreview: `# .guardviberc\n{ "doctor": { "trustedBaseUrls": ["${url}"] } }`,
179
+ });
180
+ }
181
+ }