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,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
+ }
@@ -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
+ }
package/package.json CHANGED
@@ -1,12 +1,11 @@
1
1
  {
2
2
  "name": "guardvibe",
3
- "version": "2.4.5",
3
+ "version": "2.7.3",
4
4
  "mcpName": "io.github.goklab/guardvibe",
5
- "description": "Security MCP for vibe coding. 313 rules, 25 tools for Next.js, Supabase, Clerk, Stripe, Prisma, tRPC, Hono, GraphQL, Convex, Turso, Uploadthing, AI SDK, and the full AI-generated stack.",
5
+ "description": "Security MCP for vibe coding. 330 rules, 29 tools, CLI + doctor. Host security: CVE-2025-59536 hook injection, CVE-2026-21852 base URL hijack, MCP config audit, AI host hardening. Plus Next.js, Supabase, Clerk, Stripe, Prisma, tRPC, Hono, GraphQL, Convex, Turso, Uploadthing, AI SDK, and the full AI-generated stack.",
6
6
  "type": "module",
7
7
  "bin": {
8
8
  "guardvibe": "build/cli.js",
9
- "guardvibe-init": "build/cli.js",
10
9
  "guardvibe-scan": "build/cli.js"
11
10
  },
12
11
  "main": "./build/index.js",
@@ -109,7 +108,7 @@
109
108
  "zod": "^3.25.0"
110
109
  },
111
110
  "devDependencies": {
112
- "@types/node": "^22.0.0",
111
+ "@types/node": "^25.5.2",
113
112
  "c8": "^11.0.0",
114
113
  "eslint": "^10.2.0",
115
114
  "tsx": "^4.21.0",