@bddiudiu/vibeguard 0.1.0
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/.github/workflows/publish.yml +40 -0
- package/.vibeguard.yaml +49 -0
- package/README.md +203 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +47 -0
- package/dist/cli.js.map +1 -0
- package/dist/connectors/ollama.d.ts +7 -0
- package/dist/connectors/ollama.d.ts.map +1 -0
- package/dist/connectors/ollama.js +26 -0
- package/dist/connectors/ollama.js.map +1 -0
- package/dist/connectors/openai.d.ts +8 -0
- package/dist/connectors/openai.d.ts.map +1 -0
- package/dist/connectors/openai.js +20 -0
- package/dist/connectors/openai.js.map +1 -0
- package/dist/core/analyzer.d.ts +12 -0
- package/dist/core/analyzer.d.ts.map +1 -0
- package/dist/core/analyzer.js +38 -0
- package/dist/core/analyzer.js.map +1 -0
- package/dist/core/audit.d.ts +9 -0
- package/dist/core/audit.d.ts.map +1 -0
- package/dist/core/audit.js +52 -0
- package/dist/core/audit.js.map +1 -0
- package/dist/core/config.d.ts +27 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +46 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/git.d.ts +17 -0
- package/dist/core/git.d.ts.map +1 -0
- package/dist/core/git.js +92 -0
- package/dist/core/git.js.map +1 -0
- package/dist/rules/hallucination.d.ts +7 -0
- package/dist/rules/hallucination.d.ts.map +1 -0
- package/dist/rules/hallucination.js +20 -0
- package/dist/rules/hallucination.js.map +1 -0
- package/dist/rules/index.d.ts +3 -0
- package/dist/rules/index.d.ts.map +1 -0
- package/dist/rules/index.js +27 -0
- package/dist/rules/index.js.map +1 -0
- package/dist/rules/security.d.ts +7 -0
- package/dist/rules/security.d.ts.map +1 -0
- package/dist/rules/security.js +19 -0
- package/dist/rules/security.js.map +1 -0
- package/package.json +54 -0
- package/src/cli.ts +61 -0
- package/src/connectors/ollama.ts +38 -0
- package/src/connectors/openai.ts +32 -0
- package/src/core/analyzer.ts +68 -0
- package/src/core/audit.ts +72 -0
- package/src/core/config.ts +76 -0
- package/src/core/git.ts +108 -0
- package/src/rules/hallucination.ts +27 -0
- package/src/rules/index.ts +37 -0
- package/src/rules/security.ts +25 -0
- package/tests/hallucination.test.ts +93 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface DiffEntry {
|
|
2
|
+
filePath: string;
|
|
3
|
+
diff: string;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* 从暂存区提取 diff,过滤非代码文件。
|
|
7
|
+
*/
|
|
8
|
+
export declare function getCachedDiff(ignorePaths?: string[]): DiffEntry[];
|
|
9
|
+
/**
|
|
10
|
+
* 安装 pre-commit hook
|
|
11
|
+
*/
|
|
12
|
+
export declare function installHook(): void;
|
|
13
|
+
/**
|
|
14
|
+
* 卸载 pre-commit hook
|
|
15
|
+
*/
|
|
16
|
+
export declare function uninstallHook(): void;
|
|
17
|
+
//# sourceMappingURL=git.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git.d.ts","sourceRoot":"","sources":["../../src/core/git.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd;AAWD;;GAEG;AACH,wBAAgB,aAAa,CAAC,WAAW,GAAE,MAAM,EAAO,GAAG,SAAS,EAAE,CAsCrE;AAED;;GAEG;AACH,wBAAgB,WAAW,IAAI,IAAI,CAiBlC;AAED;;GAEG;AACH,wBAAgB,aAAa,IAAI,IAAI,CAcpC"}
|
package/dist/core/git.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, chmodSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
const HOOK_PATH = ".git/hooks/pre-commit";
|
|
5
|
+
const HOOK_CONTENT = `#!/bin/sh
|
|
6
|
+
# VibeGuard pre-commit hook
|
|
7
|
+
echo "🛡️ VibeGuard: scanning staged changes..."
|
|
8
|
+
npx vibeguard scan
|
|
9
|
+
exit $?
|
|
10
|
+
`;
|
|
11
|
+
/**
|
|
12
|
+
* 从暂存区提取 diff,过滤非代码文件。
|
|
13
|
+
*/
|
|
14
|
+
export function getCachedDiff(ignorePaths = []) {
|
|
15
|
+
const names = execSync("git diff --cached --name-only --diff-filter=ACMR", {
|
|
16
|
+
encoding: "utf-8",
|
|
17
|
+
})
|
|
18
|
+
.trim()
|
|
19
|
+
.split("\n")
|
|
20
|
+
.filter(Boolean);
|
|
21
|
+
if (names.length === 0)
|
|
22
|
+
return [];
|
|
23
|
+
const binaryExts = [
|
|
24
|
+
".png", ".jpg", ".jpeg", ".gif", ".ico", ".svg",
|
|
25
|
+
".woff", ".woff2", ".ttf", ".eot",
|
|
26
|
+
".zip", ".tar", ".gz", ".rar",
|
|
27
|
+
".pdf", ".exe", ".bin",
|
|
28
|
+
".mp3", ".mp4", ".avi",
|
|
29
|
+
];
|
|
30
|
+
const entries = [];
|
|
31
|
+
for (const name of names) {
|
|
32
|
+
if (binaryExts.some((ext) => name.endsWith(ext)))
|
|
33
|
+
continue;
|
|
34
|
+
if (ignorePaths.some((p) => matchGlob(name, p)))
|
|
35
|
+
continue;
|
|
36
|
+
try {
|
|
37
|
+
const diff = execSync(`git diff --cached -- "${name}"`, {
|
|
38
|
+
encoding: "utf-8",
|
|
39
|
+
maxBuffer: 1024 * 1024 * 2,
|
|
40
|
+
});
|
|
41
|
+
if (diff.trim()) {
|
|
42
|
+
entries.push({ filePath: name, diff });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// 跳过无法读取的文件 (如新增的二进制文件)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return entries;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* 安装 pre-commit hook
|
|
53
|
+
*/
|
|
54
|
+
export function installHook() {
|
|
55
|
+
const gitDir = execSync("git rev-parse --show-toplevel", {
|
|
56
|
+
encoding: "utf-8",
|
|
57
|
+
}).trim();
|
|
58
|
+
const hookPath = join(gitDir, ".git", "hooks", "pre-commit");
|
|
59
|
+
if (existsSync(hookPath)) {
|
|
60
|
+
const existing = readFileSync(hookPath, "utf-8");
|
|
61
|
+
if (existing.includes("VibeGuard")) {
|
|
62
|
+
return; // 已安装
|
|
63
|
+
}
|
|
64
|
+
// 备份已有 hook
|
|
65
|
+
writeFileSync(hookPath + ".bak", existing);
|
|
66
|
+
}
|
|
67
|
+
writeFileSync(hookPath, HOOK_CONTENT);
|
|
68
|
+
chmodSync(hookPath, 0o755);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* 卸载 pre-commit hook
|
|
72
|
+
*/
|
|
73
|
+
export function uninstallHook() {
|
|
74
|
+
const gitDir = execSync("git rev-parse --show-toplevel", {
|
|
75
|
+
encoding: "utf-8",
|
|
76
|
+
}).trim();
|
|
77
|
+
const hookPath = join(gitDir, ".git", "hooks", "pre-commit");
|
|
78
|
+
const hookPathBak = hookPath + ".bak";
|
|
79
|
+
if (existsSync(hookPathBak)) {
|
|
80
|
+
const { renameSync } = require("fs");
|
|
81
|
+
renameSync(hookPathBak, hookPath);
|
|
82
|
+
}
|
|
83
|
+
else if (existsSync(hookPath)) {
|
|
84
|
+
const { unlinkSync } = require("fs");
|
|
85
|
+
unlinkSync(hookPath);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function matchGlob(name, pattern) {
|
|
89
|
+
const regex = new RegExp("^" + pattern.replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*") + "$");
|
|
90
|
+
return regex.test(name);
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=git.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git.js","sourceRoot":"","sources":["../../src/core/git.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hallucination.d.ts","sourceRoot":"","sources":["../../src/rules/hallucination.ts"],"names":[],"mappings":"AAAA,UAAU,WAAW;IACnB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,cAAc,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,CAqB9D"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function hallucinationPrompt(rules) {
|
|
2
|
+
let prompt = `## 幻觉检测规则
|
|
3
|
+
|
|
4
|
+
请重点检查以下 AI 常见的幻觉特征:
|
|
5
|
+
|
|
6
|
+
1. **虚假 API 调用**: AI 编造了不存在的函数、方法或参数。例如: requests.get_with_auto_retry()、lodash.deepClone() (注意 lodash 中实际是 cloneDeep)
|
|
7
|
+
2. **逻辑矛盾**: 代码中存在前后矛盾的条件判断或变量使用
|
|
8
|
+
3. **未实现的占位代码**: TODO、FIXME、placeholder、stub 等占位标记
|
|
9
|
+
4. **虚构的库引用**: 引入了不存在的 npm 包或 Python 模块
|
|
10
|
+
5. **类型/接口不匹配**: 函数签名与调用方式不一致
|
|
11
|
+
6. **API 版本错误**: 使用了已废弃或不存在的 API 版本`;
|
|
12
|
+
if (rules.bannedImports.length > 0) {
|
|
13
|
+
prompt += `\n\n**禁止导入的库**: ${rules.bannedImports.join(", ")}`;
|
|
14
|
+
}
|
|
15
|
+
if (rules.bannedPatterns.length > 0) {
|
|
16
|
+
prompt += `\n\n**禁止使用的代码模式**: ${rules.bannedPatterns.join(", ")}`;
|
|
17
|
+
}
|
|
18
|
+
return prompt;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=hallucination.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hallucination.js","sourceRoot":"","sources":["../../src/rules/hallucination.ts"],"names":[],"mappings":"AAKA,MAAM,UAAU,mBAAmB,CAAC,KAAkB;IACpD,IAAI,MAAM,GAAG;;;;;;;;;oCASqB,CAAC;IAEnC,IAAI,KAAK,CAAC,aAAa,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACnC,MAAM,IAAI,mBAAmB,KAAK,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;IAChE,CAAC;IAED,IAAI,KAAK,CAAC,cAAc,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACpC,MAAM,IAAI,sBAAsB,KAAK,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;IACpE,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/rules/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAIpD,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,eAAe,GACtB,MAAM,CA6BR"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { hallucinationPrompt } from "./hallucination.js";
|
|
2
|
+
import { securityPrompt } from "./security.js";
|
|
3
|
+
export function buildAuditPrompt(diff, config) {
|
|
4
|
+
const sections = [];
|
|
5
|
+
sections.push("请分析以下 git diff 变更,检测代码中的幻觉错误和安全风险。\n");
|
|
6
|
+
if (config.scan.hallucinationDetection) {
|
|
7
|
+
sections.push(hallucinationPrompt(config.rules));
|
|
8
|
+
}
|
|
9
|
+
if (config.scan.securityScan) {
|
|
10
|
+
sections.push(securityPrompt(config.rules));
|
|
11
|
+
}
|
|
12
|
+
sections.push(`---\n\n以下是待分析的 diff:\n\n\`\`\`diff\n${diff}\n\`\`\``);
|
|
13
|
+
sections.push(`
|
|
14
|
+
请按照以下 JSON 格式返回检测结果(如果没有问题,返回空数组 []):
|
|
15
|
+
|
|
16
|
+
[
|
|
17
|
+
{
|
|
18
|
+
"line": 可选的行号,
|
|
19
|
+
"severity": "high" | "medium" | "low",
|
|
20
|
+
"category": "hallucination" | "security" | "banned-import" | "banned-pattern",
|
|
21
|
+
"message": "问题描述",
|
|
22
|
+
"suggestion": "可选的修复建议"
|
|
23
|
+
}
|
|
24
|
+
]`);
|
|
25
|
+
return sections.join("\n\n");
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/rules/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAE/C,MAAM,UAAU,gBAAgB,CAC9B,IAAY,EACZ,MAAuB;IAEvB,MAAM,QAAQ,GAAa,EAAE,CAAC;IAE9B,QAAQ,CAAC,IAAI,CAAC,sCAAsC,CAAC,CAAC;IAEtD,IAAI,MAAM,CAAC,IAAI,CAAC,sBAAsB,EAAE,CAAC;QACvC,QAAQ,CAAC,IAAI,CAAC,mBAAmB,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IACnD,CAAC;IAED,IAAI,MAAM,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC;QAC7B,QAAQ,CAAC,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IAC9C,CAAC;IAED,QAAQ,CAAC,IAAI,CAAC,uCAAuC,IAAI,UAAU,CAAC,CAAC;IAErE,QAAQ,CAAC,IAAI,CAAC;;;;;;;;;;;EAWd,CAAC,CAAC;IAEF,OAAO,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AAC/B,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"security.d.ts","sourceRoot":"","sources":["../../src/rules/security.ts"],"names":[],"mappings":"AAAA,UAAU,WAAW;IACnB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,cAAc,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,CAmBzD"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function securityPrompt(rules) {
|
|
2
|
+
let prompt = `## 安全扫描规则
|
|
3
|
+
|
|
4
|
+
请重点检查以下安全风险:
|
|
5
|
+
|
|
6
|
+
1. **硬编码密钥**: 检测 API Key、Secret、Token、密码等敏感信息被直接写入代码
|
|
7
|
+
- 匹配模式: key=xxx, token=xxx, secret=xxx, password=xxx, api_key=xxx
|
|
8
|
+
- 常见格式: sk-xxx, AKIAxxx, ghpxxxx, xoxb-xxx
|
|
9
|
+
2. **注入风险**: SQL 注入、命令注入、XSS 等明显的注入漏洞
|
|
10
|
+
- 例如: 字符串拼接 SQL、eval() 执行用户输入、innerHTML 赋值
|
|
11
|
+
3. **不安全的加密**: 使用 MD5、SHA1 等已知不安全的哈希算法
|
|
12
|
+
4. **敏感信息泄露**: 将凭据输出到日志或返回给前端
|
|
13
|
+
5. **危险函数调用**: eval(), new Function(), exec(), spawn() 等高危函数`;
|
|
14
|
+
if (rules.bannedPatterns.length > 0) {
|
|
15
|
+
prompt += `\n\n**禁止使用的代码模式**: ${rules.bannedPatterns.join(", ")}`;
|
|
16
|
+
}
|
|
17
|
+
return prompt;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=security.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"security.js","sourceRoot":"","sources":["../../src/rules/security.ts"],"names":[],"mappings":"AAKA,MAAM,UAAU,cAAc,CAAC,KAAkB;IAC/C,IAAI,MAAM,GAAG;;;;;;;;;;;6DAW8C,CAAC;IAE5D,IAAI,KAAK,CAAC,cAAc,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACpC,MAAM,IAAI,sBAAsB,KAAK,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;IACpE,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bddiudiu/vibeguard",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "AI 代码生成的语义安检门,在 git commit 前自动拦截幻觉错误与安全隐患",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/bddiudiu/vibeguard"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"main": "dist/cli.js",
|
|
11
|
+
"bin": {
|
|
12
|
+
"vibeguard": "dist/cli.js"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"dev": "tsc --watch",
|
|
17
|
+
"lint": "eslint src/",
|
|
18
|
+
"test": "vitest run",
|
|
19
|
+
"test:watch": "vitest",
|
|
20
|
+
"vibeguard:scan": "node dist/cli.js scan",
|
|
21
|
+
"prepare": "husky"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"ai",
|
|
25
|
+
"code-review",
|
|
26
|
+
"git-hook",
|
|
27
|
+
"hallucination",
|
|
28
|
+
"security",
|
|
29
|
+
"ollama",
|
|
30
|
+
"openai",
|
|
31
|
+
"lint",
|
|
32
|
+
"pre-commit"
|
|
33
|
+
],
|
|
34
|
+
"author": "",
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"chalk": "^5.3.0",
|
|
38
|
+
"commander": "^12.1.0",
|
|
39
|
+
"js-yaml": "^4.1.0",
|
|
40
|
+
"openai": "^4.52.0",
|
|
41
|
+
"better-sqlite3": "^11.1.2"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/better-sqlite3": "^7.6.11",
|
|
45
|
+
"@types/js-yaml": "^4.0.9",
|
|
46
|
+
"@types/node": "^20.14.10",
|
|
47
|
+
"husky": "^9.1.1",
|
|
48
|
+
"typescript": "^5.5.3",
|
|
49
|
+
"vitest": "^2.0.5"
|
|
50
|
+
},
|
|
51
|
+
"engines": {
|
|
52
|
+
"node": ">=18.0.0"
|
|
53
|
+
}
|
|
54
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import { runScan } from "./core/audit.js";
|
|
6
|
+
import { installHook, uninstallHook } from "./core/git.js";
|
|
7
|
+
import { loadConfig } from "./core/config.js";
|
|
8
|
+
|
|
9
|
+
const program = new Command();
|
|
10
|
+
|
|
11
|
+
program
|
|
12
|
+
.name("vibeguard")
|
|
13
|
+
.description("AI 代码生成的语义安检门 — 在 git commit 前拦截幻觉与安全隐患")
|
|
14
|
+
.version("0.1.0");
|
|
15
|
+
|
|
16
|
+
program
|
|
17
|
+
.command("scan")
|
|
18
|
+
.description("扫描当前暂存区的变更")
|
|
19
|
+
.option("--no-verify", "强制扫描,不阻止 commit")
|
|
20
|
+
.action(async (opts) => {
|
|
21
|
+
const config = await loadConfig();
|
|
22
|
+
const result = await runScan(config);
|
|
23
|
+
|
|
24
|
+
if (!result.passed && opts.verify !== false) {
|
|
25
|
+
console.log(
|
|
26
|
+
chalk.red.bold(
|
|
27
|
+
"\n🛡️ VibeGuard 拦截了本次提交,请修复上述问题后重试。"
|
|
28
|
+
)
|
|
29
|
+
);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
program
|
|
35
|
+
.command("init")
|
|
36
|
+
.description("在当前项目安装 git pre-commit hook")
|
|
37
|
+
.action(async () => {
|
|
38
|
+
await installHook();
|
|
39
|
+
console.log(chalk.green("✅ pre-commit hook 已安装。"));
|
|
40
|
+
console.log(
|
|
41
|
+
chalk.dim(" 提交时 VibeGuard 将自动扫描变更。使用 --no-verify 可跳过。")
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
program
|
|
46
|
+
.command("uninstall")
|
|
47
|
+
.description("卸载 git pre-commit hook")
|
|
48
|
+
.action(async () => {
|
|
49
|
+
await uninstallHook();
|
|
50
|
+
console.log(chalk.yellow("⚠️ pre-commit hook 已卸载。"));
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
program
|
|
54
|
+
.command("config")
|
|
55
|
+
.description("显示当前生效的配置")
|
|
56
|
+
.action(async () => {
|
|
57
|
+
const config = await loadConfig();
|
|
58
|
+
console.log(JSON.stringify(config, null, 2));
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
program.parse();
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
interface OllamaConfig {
|
|
2
|
+
model: string;
|
|
3
|
+
baseUrl: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export async function callOllama(
|
|
7
|
+
prompt: string,
|
|
8
|
+
config: OllamaConfig
|
|
9
|
+
): Promise<string> {
|
|
10
|
+
const response = await fetch(`${config.baseUrl}/api/chat`, {
|
|
11
|
+
method: "POST",
|
|
12
|
+
headers: { "Content-Type": "application/json" },
|
|
13
|
+
body: JSON.stringify({
|
|
14
|
+
model: config.model,
|
|
15
|
+
messages: [
|
|
16
|
+
{
|
|
17
|
+
role: "system",
|
|
18
|
+
content:
|
|
19
|
+
"你是代码审计专家。请严格以 JSON 数组格式返回检测结果,不要包含任何其他文本。如果没有发现问题,返回空数组 []。",
|
|
20
|
+
},
|
|
21
|
+
{ role: "user", content: prompt },
|
|
22
|
+
],
|
|
23
|
+
stream: false,
|
|
24
|
+
options: {
|
|
25
|
+
temperature: 0.1,
|
|
26
|
+
},
|
|
27
|
+
}),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
if (!response.ok) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
`Ollama API error: ${response.status} ${response.statusText}`
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const data = (await response.json()) as { message?: { content?: string } };
|
|
37
|
+
return data.message?.content ?? "[]";
|
|
38
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import OpenAI from "openai";
|
|
2
|
+
|
|
3
|
+
interface OpenAIConfig {
|
|
4
|
+
model: string;
|
|
5
|
+
apiKey?: string;
|
|
6
|
+
baseUrl?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function callOpenAI(
|
|
10
|
+
prompt: string,
|
|
11
|
+
config: OpenAIConfig
|
|
12
|
+
): Promise<string> {
|
|
13
|
+
const client = new OpenAI({
|
|
14
|
+
apiKey: config.apiKey || process.env.OPENAI_API_KEY,
|
|
15
|
+
baseURL: config.baseUrl,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const response = await client.chat.completions.create({
|
|
19
|
+
model: config.model,
|
|
20
|
+
temperature: 0.1,
|
|
21
|
+
messages: [
|
|
22
|
+
{
|
|
23
|
+
role: "system",
|
|
24
|
+
content:
|
|
25
|
+
"你是代码审计专家。请严格以 JSON 数组格式返回检测结果,不要包含任何其他文本。如果没有发现问题,返回空数组 []。",
|
|
26
|
+
},
|
|
27
|
+
{ role: "user", content: prompt },
|
|
28
|
+
],
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
return response.choices[0]?.message?.content ?? "[]";
|
|
32
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { DiffEntry } from "./git.js";
|
|
2
|
+
import { VibeguardConfig } from "./config.js";
|
|
3
|
+
import { callOpenAI } from "../connectors/openai.js";
|
|
4
|
+
import { callOllama } from "../connectors/ollama.js";
|
|
5
|
+
import { buildAuditPrompt } from "../rules/index.js";
|
|
6
|
+
|
|
7
|
+
export interface AuditResult {
|
|
8
|
+
file: string;
|
|
9
|
+
line?: number;
|
|
10
|
+
severity: "high" | "medium" | "low";
|
|
11
|
+
category: string;
|
|
12
|
+
message: string;
|
|
13
|
+
suggestion?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface RawAuditItem {
|
|
17
|
+
line?: number;
|
|
18
|
+
severity: "high" | "medium" | "low";
|
|
19
|
+
category: string;
|
|
20
|
+
message: string;
|
|
21
|
+
suggestion?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function analyzeWithAI(
|
|
25
|
+
entry: DiffEntry,
|
|
26
|
+
config: VibeguardConfig
|
|
27
|
+
): Promise<AuditResult[]> {
|
|
28
|
+
const prompt = buildAuditPrompt(entry.diff, config);
|
|
29
|
+
const timeout = config.scan.timeout * 1000;
|
|
30
|
+
|
|
31
|
+
const raw = await Promise.race([
|
|
32
|
+
callAI(prompt, config),
|
|
33
|
+
new Promise<never>((_, reject) =>
|
|
34
|
+
setTimeout(() => reject(new Error("AI scan timeout")), timeout)
|
|
35
|
+
),
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
return parseResults(entry.filePath, raw);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function callAI(
|
|
42
|
+
prompt: string,
|
|
43
|
+
config: VibeguardConfig
|
|
44
|
+
): Promise<string> {
|
|
45
|
+
if (config.model.provider === "ollama") {
|
|
46
|
+
return callOllama(prompt, config.model.ollama);
|
|
47
|
+
}
|
|
48
|
+
return callOpenAI(prompt, config.model.openai);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function parseResults(filePath: string, raw: string): AuditResult[] {
|
|
52
|
+
const jsonMatch = raw.match(/\[[\s\S]*\]/);
|
|
53
|
+
if (!jsonMatch) return [];
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const items: RawAuditItem[] = JSON.parse(jsonMatch[0]);
|
|
57
|
+
return items.map((item) => ({
|
|
58
|
+
file: filePath,
|
|
59
|
+
line: item.line,
|
|
60
|
+
severity: item.severity,
|
|
61
|
+
category: item.category,
|
|
62
|
+
message: item.message,
|
|
63
|
+
suggestion: item.suggestion,
|
|
64
|
+
}));
|
|
65
|
+
} catch {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { getCachedDiff, DiffEntry } from "./git.js";
|
|
3
|
+
import { loadConfig, VibeguardConfig } from "./config.js";
|
|
4
|
+
import { analyzeWithAI, AuditResult } from "./analyzer.js";
|
|
5
|
+
|
|
6
|
+
export interface ScanResult {
|
|
7
|
+
passed: boolean;
|
|
8
|
+
issues: AuditResult[];
|
|
9
|
+
scannedFiles: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function runScan(config: VibeguardConfig): Promise<ScanResult> {
|
|
13
|
+
const entries = getCachedDiff(config.rules.ignorePaths);
|
|
14
|
+
|
|
15
|
+
if (entries.length === 0) {
|
|
16
|
+
console.log(chalk.dim("🛡️ VibeGuard: 暂存区无代码变更,跳过扫描。"));
|
|
17
|
+
return { passed: true, issues: [], scannedFiles: 0 };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
console.log(
|
|
21
|
+
chalk.cyan(`🛡️ VibeGuard: 发现 ${entries.length} 个变更文件,开始扫描...\n`)
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const allIssues: AuditResult[] = [];
|
|
25
|
+
|
|
26
|
+
for (const entry of entries) {
|
|
27
|
+
const truncated = truncateDiff(entry, config.scan.maxDiffChars);
|
|
28
|
+
const issues = await analyzeWithAI(truncated, config);
|
|
29
|
+
allIssues.push(...issues);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
printReport(allIssues);
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
passed: allIssues.filter((i) => i.severity === "high").length === 0,
|
|
36
|
+
issues: allIssues,
|
|
37
|
+
scannedFiles: entries.length,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function truncateDiff(entry: DiffEntry, maxChars: number): DiffEntry {
|
|
42
|
+
if (entry.diff.length <= maxChars) return entry;
|
|
43
|
+
return {
|
|
44
|
+
filePath: entry.filePath,
|
|
45
|
+
diff: entry.diff.slice(0, maxChars) + "\n... (diff truncated)",
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function printReport(issues: AuditResult[]): void {
|
|
50
|
+
if (issues.length === 0) {
|
|
51
|
+
console.log(chalk.green.bold("✅ VibeGuard: 未检测到风险,可以安全提交。"));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
console.log(chalk.yellow.bold(`\n⚠️ 检测到 ${issues.length} 个问题:\n`));
|
|
56
|
+
|
|
57
|
+
for (const issue of issues) {
|
|
58
|
+
const icon =
|
|
59
|
+
issue.severity === "high"
|
|
60
|
+
? chalk.red.bold("🚨 HIGH")
|
|
61
|
+
: issue.severity === "medium"
|
|
62
|
+
? chalk.yellow("⚠️ MED ")
|
|
63
|
+
: chalk.blue("ℹ️ LOW ");
|
|
64
|
+
|
|
65
|
+
console.log(`${icon} ${chalk.bold(issue.file)}:${issue.line ?? "?"}`);
|
|
66
|
+
console.log(` ${issue.message}`);
|
|
67
|
+
if (issue.suggestion) {
|
|
68
|
+
console.log(chalk.dim(` 💡 建议: ${issue.suggestion}`));
|
|
69
|
+
}
|
|
70
|
+
console.log();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import yaml from "js-yaml";
|
|
4
|
+
|
|
5
|
+
export interface VibeguardConfig {
|
|
6
|
+
model: {
|
|
7
|
+
provider: "openai" | "ollama";
|
|
8
|
+
openai: {
|
|
9
|
+
model: string;
|
|
10
|
+
apiKey?: string;
|
|
11
|
+
baseUrl?: string;
|
|
12
|
+
};
|
|
13
|
+
ollama: {
|
|
14
|
+
model: string;
|
|
15
|
+
baseUrl: string;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
scan: {
|
|
19
|
+
maxDiffChars: number;
|
|
20
|
+
hallucinationDetection: boolean;
|
|
21
|
+
securityScan: boolean;
|
|
22
|
+
timeout: number;
|
|
23
|
+
};
|
|
24
|
+
rules: {
|
|
25
|
+
bannedImports: string[];
|
|
26
|
+
bannedPatterns: string[];
|
|
27
|
+
ignorePaths: string[];
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const DEFAULT_CONFIG: VibeguardConfig = {
|
|
32
|
+
model: {
|
|
33
|
+
provider: "ollama",
|
|
34
|
+
openai: {
|
|
35
|
+
model: "gpt-4o-mini",
|
|
36
|
+
// 默认使用 OpenAI 官方 API,不设置 baseUrl
|
|
37
|
+
},
|
|
38
|
+
ollama: {
|
|
39
|
+
model: "llama3",
|
|
40
|
+
baseUrl: "http://localhost:11434",
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
scan: {
|
|
44
|
+
maxDiffChars: 12000,
|
|
45
|
+
hallucinationDetection: true,
|
|
46
|
+
securityScan: true,
|
|
47
|
+
timeout: 30,
|
|
48
|
+
},
|
|
49
|
+
rules: {
|
|
50
|
+
bannedImports: [],
|
|
51
|
+
bannedPatterns: [],
|
|
52
|
+
ignorePaths: ["dist/**", "node_modules/**"],
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export async function loadConfig(): Promise<VibeguardConfig> {
|
|
57
|
+
const configPath = join(process.cwd(), ".vibeguard.yaml");
|
|
58
|
+
|
|
59
|
+
if (!existsSync(configPath)) {
|
|
60
|
+
return DEFAULT_CONFIG;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
64
|
+
const userConfig = yaml.load(raw) as Partial<VibeguardConfig>;
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
model: {
|
|
68
|
+
...DEFAULT_CONFIG.model,
|
|
69
|
+
...userConfig?.model,
|
|
70
|
+
openai: { ...DEFAULT_CONFIG.model.openai, ...userConfig?.model?.openai },
|
|
71
|
+
ollama: { ...DEFAULT_CONFIG.model.ollama, ...userConfig?.model?.ollama },
|
|
72
|
+
},
|
|
73
|
+
scan: { ...DEFAULT_CONFIG.scan, ...userConfig?.scan },
|
|
74
|
+
rules: { ...DEFAULT_CONFIG.rules, ...userConfig?.rules },
|
|
75
|
+
};
|
|
76
|
+
}
|