@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.
Files changed (56) hide show
  1. package/.github/workflows/publish.yml +40 -0
  2. package/.vibeguard.yaml +49 -0
  3. package/README.md +203 -0
  4. package/dist/cli.d.ts +3 -0
  5. package/dist/cli.d.ts.map +1 -0
  6. package/dist/cli.js +47 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/connectors/ollama.d.ts +7 -0
  9. package/dist/connectors/ollama.d.ts.map +1 -0
  10. package/dist/connectors/ollama.js +26 -0
  11. package/dist/connectors/ollama.js.map +1 -0
  12. package/dist/connectors/openai.d.ts +8 -0
  13. package/dist/connectors/openai.d.ts.map +1 -0
  14. package/dist/connectors/openai.js +20 -0
  15. package/dist/connectors/openai.js.map +1 -0
  16. package/dist/core/analyzer.d.ts +12 -0
  17. package/dist/core/analyzer.d.ts.map +1 -0
  18. package/dist/core/analyzer.js +38 -0
  19. package/dist/core/analyzer.js.map +1 -0
  20. package/dist/core/audit.d.ts +9 -0
  21. package/dist/core/audit.d.ts.map +1 -0
  22. package/dist/core/audit.js +52 -0
  23. package/dist/core/audit.js.map +1 -0
  24. package/dist/core/config.d.ts +27 -0
  25. package/dist/core/config.d.ts.map +1 -0
  26. package/dist/core/config.js +46 -0
  27. package/dist/core/config.js.map +1 -0
  28. package/dist/core/git.d.ts +17 -0
  29. package/dist/core/git.d.ts.map +1 -0
  30. package/dist/core/git.js +92 -0
  31. package/dist/core/git.js.map +1 -0
  32. package/dist/rules/hallucination.d.ts +7 -0
  33. package/dist/rules/hallucination.d.ts.map +1 -0
  34. package/dist/rules/hallucination.js +20 -0
  35. package/dist/rules/hallucination.js.map +1 -0
  36. package/dist/rules/index.d.ts +3 -0
  37. package/dist/rules/index.d.ts.map +1 -0
  38. package/dist/rules/index.js +27 -0
  39. package/dist/rules/index.js.map +1 -0
  40. package/dist/rules/security.d.ts +7 -0
  41. package/dist/rules/security.d.ts.map +1 -0
  42. package/dist/rules/security.js +19 -0
  43. package/dist/rules/security.js.map +1 -0
  44. package/package.json +54 -0
  45. package/src/cli.ts +61 -0
  46. package/src/connectors/ollama.ts +38 -0
  47. package/src/connectors/openai.ts +32 -0
  48. package/src/core/analyzer.ts +68 -0
  49. package/src/core/audit.ts +72 -0
  50. package/src/core/config.ts +76 -0
  51. package/src/core/git.ts +108 -0
  52. package/src/rules/hallucination.ts +27 -0
  53. package/src/rules/index.ts +37 -0
  54. package/src/rules/security.ts +25 -0
  55. package/tests/hallucination.test.ts +93 -0
  56. package/tsconfig.json +19 -0
@@ -0,0 +1,108 @@
1
+ import { execSync } from "child_process";
2
+ import { existsSync, readFileSync, writeFileSync, chmodSync } from "fs";
3
+ import { join } from "path";
4
+
5
+ export interface DiffEntry {
6
+ filePath: string;
7
+ diff: string;
8
+ }
9
+
10
+ const HOOK_PATH = ".git/hooks/pre-commit";
11
+
12
+ const HOOK_CONTENT = `#!/bin/sh
13
+ # VibeGuard pre-commit hook
14
+ echo "🛡️ VibeGuard: scanning staged changes..."
15
+ npx vibeguard scan
16
+ exit $?
17
+ `;
18
+
19
+ /**
20
+ * 从暂存区提取 diff,过滤非代码文件。
21
+ */
22
+ export function getCachedDiff(ignorePaths: string[] = []): DiffEntry[] {
23
+ const names = execSync("git diff --cached --name-only --diff-filter=ACMR", {
24
+ encoding: "utf-8",
25
+ })
26
+ .trim()
27
+ .split("\n")
28
+ .filter(Boolean);
29
+
30
+ if (names.length === 0) return [];
31
+
32
+ const binaryExts = [
33
+ ".png", ".jpg", ".jpeg", ".gif", ".ico", ".svg",
34
+ ".woff", ".woff2", ".ttf", ".eot",
35
+ ".zip", ".tar", ".gz", ".rar",
36
+ ".pdf", ".exe", ".bin",
37
+ ".mp3", ".mp4", ".avi",
38
+ ];
39
+
40
+ const entries: DiffEntry[] = [];
41
+
42
+ for (const name of names) {
43
+ if (binaryExts.some((ext) => name.endsWith(ext))) continue;
44
+ if (ignorePaths.some((p) => matchGlob(name, p))) continue;
45
+
46
+ try {
47
+ const diff = execSync(`git diff --cached -- "${name}"`, {
48
+ encoding: "utf-8",
49
+ maxBuffer: 1024 * 1024 * 2,
50
+ });
51
+ if (diff.trim()) {
52
+ entries.push({ filePath: name, diff });
53
+ }
54
+ } catch {
55
+ // 跳过无法读取的文件 (如新增的二进制文件)
56
+ }
57
+ }
58
+
59
+ return entries;
60
+ }
61
+
62
+ /**
63
+ * 安装 pre-commit hook
64
+ */
65
+ export function installHook(): void {
66
+ const gitDir = execSync("git rev-parse --show-toplevel", {
67
+ encoding: "utf-8",
68
+ }).trim();
69
+ const hookPath = join(gitDir, ".git", "hooks", "pre-commit");
70
+
71
+ if (existsSync(hookPath)) {
72
+ const existing = readFileSync(hookPath, "utf-8");
73
+ if (existing.includes("VibeGuard")) {
74
+ return; // 已安装
75
+ }
76
+ // 备份已有 hook
77
+ writeFileSync(hookPath + ".bak", existing);
78
+ }
79
+
80
+ writeFileSync(hookPath, HOOK_CONTENT);
81
+ chmodSync(hookPath, 0o755);
82
+ }
83
+
84
+ /**
85
+ * 卸载 pre-commit hook
86
+ */
87
+ export function uninstallHook(): void {
88
+ const gitDir = execSync("git rev-parse --show-toplevel", {
89
+ encoding: "utf-8",
90
+ }).trim();
91
+ const hookPath = join(gitDir, ".git", "hooks", "pre-commit");
92
+ const hookPathBak = hookPath + ".bak";
93
+
94
+ if (existsSync(hookPathBak)) {
95
+ const { renameSync } = require("fs");
96
+ renameSync(hookPathBak, hookPath);
97
+ } else if (existsSync(hookPath)) {
98
+ const { unlinkSync } = require("fs");
99
+ unlinkSync(hookPath);
100
+ }
101
+ }
102
+
103
+ function matchGlob(name: string, pattern: string): boolean {
104
+ const regex = new RegExp(
105
+ "^" + pattern.replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*") + "$"
106
+ );
107
+ return regex.test(name);
108
+ }
@@ -0,0 +1,27 @@
1
+ interface RulesConfig {
2
+ bannedImports: string[];
3
+ bannedPatterns: string[];
4
+ }
5
+
6
+ export function hallucinationPrompt(rules: RulesConfig): string {
7
+ let prompt = `## 幻觉检测规则
8
+
9
+ 请重点检查以下 AI 常见的幻觉特征:
10
+
11
+ 1. **虚假 API 调用**: AI 编造了不存在的函数、方法或参数。例如: requests.get_with_auto_retry()、lodash.deepClone() (注意 lodash 中实际是 cloneDeep)
12
+ 2. **逻辑矛盾**: 代码中存在前后矛盾的条件判断或变量使用
13
+ 3. **未实现的占位代码**: TODO、FIXME、placeholder、stub 等占位标记
14
+ 4. **虚构的库引用**: 引入了不存在的 npm 包或 Python 模块
15
+ 5. **类型/接口不匹配**: 函数签名与调用方式不一致
16
+ 6. **API 版本错误**: 使用了已废弃或不存在的 API 版本`;
17
+
18
+ if (rules.bannedImports.length > 0) {
19
+ prompt += `\n\n**禁止导入的库**: ${rules.bannedImports.join(", ")}`;
20
+ }
21
+
22
+ if (rules.bannedPatterns.length > 0) {
23
+ prompt += `\n\n**禁止使用的代码模式**: ${rules.bannedPatterns.join(", ")}`;
24
+ }
25
+
26
+ return prompt;
27
+ }
@@ -0,0 +1,37 @@
1
+ import { VibeguardConfig } from "../core/config.js";
2
+ import { hallucinationPrompt } from "./hallucination.js";
3
+ import { securityPrompt } from "./security.js";
4
+
5
+ export function buildAuditPrompt(
6
+ diff: string,
7
+ config: VibeguardConfig
8
+ ): string {
9
+ const sections: string[] = [];
10
+
11
+ sections.push("请分析以下 git diff 变更,检测代码中的幻觉错误和安全风险。\n");
12
+
13
+ if (config.scan.hallucinationDetection) {
14
+ sections.push(hallucinationPrompt(config.rules));
15
+ }
16
+
17
+ if (config.scan.securityScan) {
18
+ sections.push(securityPrompt(config.rules));
19
+ }
20
+
21
+ sections.push(`---\n\n以下是待分析的 diff:\n\n\`\`\`diff\n${diff}\n\`\`\``);
22
+
23
+ sections.push(`
24
+ 请按照以下 JSON 格式返回检测结果(如果没有问题,返回空数组 []):
25
+
26
+ [
27
+ {
28
+ "line": 可选的行号,
29
+ "severity": "high" | "medium" | "low",
30
+ "category": "hallucination" | "security" | "banned-import" | "banned-pattern",
31
+ "message": "问题描述",
32
+ "suggestion": "可选的修复建议"
33
+ }
34
+ ]`);
35
+
36
+ return sections.join("\n\n");
37
+ }
@@ -0,0 +1,25 @@
1
+ interface RulesConfig {
2
+ bannedImports: string[];
3
+ bannedPatterns: string[];
4
+ }
5
+
6
+ export function securityPrompt(rules: RulesConfig): string {
7
+ let prompt = `## 安全扫描规则
8
+
9
+ 请重点检查以下安全风险:
10
+
11
+ 1. **硬编码密钥**: 检测 API Key、Secret、Token、密码等敏感信息被直接写入代码
12
+ - 匹配模式: key=xxx, token=xxx, secret=xxx, password=xxx, api_key=xxx
13
+ - 常见格式: sk-xxx, AKIAxxx, ghpxxxx, xoxb-xxx
14
+ 2. **注入风险**: SQL 注入、命令注入、XSS 等明显的注入漏洞
15
+ - 例如: 字符串拼接 SQL、eval() 执行用户输入、innerHTML 赋值
16
+ 3. **不安全的加密**: 使用 MD5、SHA1 等已知不安全的哈希算法
17
+ 4. **敏感信息泄露**: 将凭据输出到日志或返回给前端
18
+ 5. **危险函数调用**: eval(), new Function(), exec(), spawn() 等高危函数`;
19
+
20
+ if (rules.bannedPatterns.length > 0) {
21
+ prompt += `\n\n**禁止使用的代码模式**: ${rules.bannedPatterns.join(", ")}`;
22
+ }
23
+
24
+ return prompt;
25
+ }
@@ -0,0 +1,93 @@
1
+ import { describe, it, expect } from "vitest";
2
+
3
+ // 模拟幻觉检测的核心逻辑(纯规则匹配,不依赖 AI)
4
+ function detectHardcodedSecrets(diff: string): string[] {
5
+ const patterns = [
6
+ /(?:api[_-]?key|token|secret|password)\s*[:=]\s*["'][^"']{8,}["']/gi,
7
+ /sk-[a-zA-Z0-9]{20,}/g,
8
+ /AKIA[0-9A-Z]{16}/g,
9
+ /ghp_[a-zA-Z0-9]{36}/g,
10
+ /xox[bpsa]-[a-zA-Z0-9-]+/g,
11
+ ];
12
+
13
+ const findings: string[] = [];
14
+ for (const pattern of patterns) {
15
+ const matches = diff.match(pattern);
16
+ if (matches) findings.push(...matches);
17
+ }
18
+ return findings;
19
+ }
20
+
21
+ function detectDangerousPatterns(diff: string): string[] {
22
+ const patterns = [
23
+ { regex: /eval\s*\(/g, name: "eval()" },
24
+ { regex: /new\s+Function\s*\(/g, name: "new Function()" },
25
+ { regex: /__import__\s*\(\s*["']os["']\s*\)/g, name: "__import__('os')" },
26
+ ];
27
+
28
+ const findings: string[] = [];
29
+ for (const { regex, name } of patterns) {
30
+ if (regex.test(diff)) findings.push(name);
31
+ }
32
+ return findings;
33
+ }
34
+
35
+ describe("VibeGuard 本地规则检测", () => {
36
+ describe("硬编码密钥检测", () => {
37
+ it("检测到 Python 风格的 API Key", () => {
38
+ const diff = '+ api_key = "sk-1234567890abcdef1234567890abcdef"';
39
+ const results = detectHardcodedSecrets(diff);
40
+ expect(results.length).toBeGreaterThan(0);
41
+ });
42
+
43
+ it("检测到 AWS Access Key", () => {
44
+ const diff = '+ AWS_ACCESS_KEY = "AKIAIOSFODNN7EXAMPLE"';
45
+ const results = detectHardcodedSecrets(diff);
46
+ expect(results.length).toBeGreaterThan(0);
47
+ });
48
+
49
+ it("检测到 GitHub Token", () => {
50
+ const diff = '+ GITHUB_TOKEN = "ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef12"';
51
+ const results = detectHardcodedSecrets(diff);
52
+ expect(results.length).toBeGreaterThan(0);
53
+ });
54
+
55
+ it("检测到 Slack Token", () => {
56
+ const diff = '+ token = "xoxb-1234567890-1234567890123-abcdefghijklmnop"';
57
+ const results = detectHardcodedSecrets(diff);
58
+ expect(results.length).toBeGreaterThan(0);
59
+ });
60
+
61
+ it("不误报正常赋值", () => {
62
+ const diff = '+ const name = "hello world"';
63
+ const results = detectHardcodedSecrets(diff);
64
+ expect(results.length).toBe(0);
65
+ });
66
+ });
67
+
68
+ describe("危险模式检测", () => {
69
+ it("检测到 eval()", () => {
70
+ const diff = '+ eval(userInput)';
71
+ const results = detectDangerousPatterns(diff);
72
+ expect(results).toContain("eval()");
73
+ });
74
+
75
+ it("检测到 new Function()", () => {
76
+ const diff = '+ const fn = new Function("return 1")';
77
+ const results = detectDangerousPatterns(diff);
78
+ expect(results).toContain("new Function()");
79
+ });
80
+
81
+ it("检测到 Python __import__", () => {
82
+ const diff = '+ __import__("os").system(cmd)';
83
+ const results = detectDangerousPatterns(diff);
84
+ expect(results).toContain("__import__('os')");
85
+ });
86
+
87
+ it("正常代码不触发告警", () => {
88
+ const diff = '+ const x = require("lodash")';
89
+ const results = detectDangerousPatterns(diff);
90
+ expect(results.length).toBe(0);
91
+ });
92
+ });
93
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "Node16",
5
+ "moduleResolution": "Node16",
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "resolveJsonModule": true,
13
+ "declaration": true,
14
+ "declarationMap": true,
15
+ "sourceMap": true
16
+ },
17
+ "include": ["src/**/*"],
18
+ "exclude": ["node_modules", "dist", "tests"]
19
+ }