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.
- package/CHANGELOG.md +42 -0
- package/README.md +44 -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 +222 -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 +57 -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 +130 -0
- package/build/tools/audit-mcp-config.d.ts +10 -0
- package/build/tools/audit-mcp-config.js +296 -0
- package/build/tools/cross-file-taint.d.ts +81 -0
- package/build/tools/cross-file-taint.js +554 -0
- package/build/tools/doctor.d.ts +14 -0
- package/build/tools/doctor.js +123 -0
- package/build/tools/scan-host-config.d.ts +10 -0
- package/build/tools/scan-host-config.js +181 -0
- package/package.json +3 -4
|
@@ -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.
|
|
3
|
+
"version": "2.7.3",
|
|
4
4
|
"mcpName": "io.github.goklab/guardvibe",
|
|
5
|
-
"description": "Security MCP for vibe coding.
|
|
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": "^
|
|
111
|
+
"@types/node": "^25.5.2",
|
|
113
112
|
"c8": "^11.0.0",
|
|
114
113
|
"eslint": "^10.2.0",
|
|
115
114
|
"tsx": "^4.21.0",
|