@indicated/vibeguard 1.0.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 (109) hide show
  1. package/.claude/settings.local.json +5 -0
  2. package/.github/workflows/ci.yml +65 -0
  3. package/.github/workflows/release.yml +85 -0
  4. package/PROGRESS.md +192 -0
  5. package/README.md +183 -0
  6. package/dist/api/license.d.ts +13 -0
  7. package/dist/api/license.d.ts.map +1 -0
  8. package/dist/api/license.js +138 -0
  9. package/dist/api/license.js.map +1 -0
  10. package/dist/api/rules.d.ts +13 -0
  11. package/dist/api/rules.d.ts.map +1 -0
  12. package/dist/api/rules.js +57 -0
  13. package/dist/api/rules.js.map +1 -0
  14. package/dist/cli/commands/init.d.ts +3 -0
  15. package/dist/cli/commands/init.d.ts.map +1 -0
  16. package/dist/cli/commands/init.js +145 -0
  17. package/dist/cli/commands/init.js.map +1 -0
  18. package/dist/cli/commands/login.d.ts +4 -0
  19. package/dist/cli/commands/login.d.ts.map +1 -0
  20. package/dist/cli/commands/login.js +121 -0
  21. package/dist/cli/commands/login.js.map +1 -0
  22. package/dist/cli/commands/mcp.d.ts +3 -0
  23. package/dist/cli/commands/mcp.d.ts.map +1 -0
  24. package/dist/cli/commands/mcp.js +14 -0
  25. package/dist/cli/commands/mcp.js.map +1 -0
  26. package/dist/cli/commands/rules.d.ts +3 -0
  27. package/dist/cli/commands/rules.d.ts.map +1 -0
  28. package/dist/cli/commands/rules.js +52 -0
  29. package/dist/cli/commands/rules.js.map +1 -0
  30. package/dist/cli/commands/scan.d.ts +3 -0
  31. package/dist/cli/commands/scan.d.ts.map +1 -0
  32. package/dist/cli/commands/scan.js +114 -0
  33. package/dist/cli/commands/scan.js.map +1 -0
  34. package/dist/cli/config.d.ts +4 -0
  35. package/dist/cli/config.d.ts.map +1 -0
  36. package/dist/cli/config.js +88 -0
  37. package/dist/cli/config.js.map +1 -0
  38. package/dist/cli/index.d.ts +3 -0
  39. package/dist/cli/index.d.ts.map +1 -0
  40. package/dist/cli/index.js +25 -0
  41. package/dist/cli/index.js.map +1 -0
  42. package/dist/cli/output.d.ts +15 -0
  43. package/dist/cli/output.d.ts.map +1 -0
  44. package/dist/cli/output.js +152 -0
  45. package/dist/cli/output.js.map +1 -0
  46. package/dist/mcp/server.d.ts +2 -0
  47. package/dist/mcp/server.d.ts.map +1 -0
  48. package/dist/mcp/server.js +188 -0
  49. package/dist/mcp/server.js.map +1 -0
  50. package/dist/scanner/index.d.ts +15 -0
  51. package/dist/scanner/index.d.ts.map +1 -0
  52. package/dist/scanner/index.js +207 -0
  53. package/dist/scanner/index.js.map +1 -0
  54. package/dist/scanner/parsers/javascript.d.ts +12 -0
  55. package/dist/scanner/parsers/javascript.d.ts.map +1 -0
  56. package/dist/scanner/parsers/javascript.js +266 -0
  57. package/dist/scanner/parsers/javascript.js.map +1 -0
  58. package/dist/scanner/parsers/python.d.ts +3 -0
  59. package/dist/scanner/parsers/python.d.ts.map +1 -0
  60. package/dist/scanner/parsers/python.js +108 -0
  61. package/dist/scanner/parsers/python.js.map +1 -0
  62. package/dist/scanner/rules/definitions.d.ts +5 -0
  63. package/dist/scanner/rules/definitions.d.ts.map +1 -0
  64. package/dist/scanner/rules/definitions.js +584 -0
  65. package/dist/scanner/rules/definitions.js.map +1 -0
  66. package/dist/scanner/rules/loader.d.ts +8 -0
  67. package/dist/scanner/rules/loader.d.ts.map +1 -0
  68. package/dist/scanner/rules/loader.js +45 -0
  69. package/dist/scanner/rules/loader.js.map +1 -0
  70. package/dist/scanner/rules/matcher.d.ts +11 -0
  71. package/dist/scanner/rules/matcher.d.ts.map +1 -0
  72. package/dist/scanner/rules/matcher.js +53 -0
  73. package/dist/scanner/rules/matcher.js.map +1 -0
  74. package/dist/types.d.ts +33 -0
  75. package/dist/types.d.ts.map +1 -0
  76. package/dist/types.js +3 -0
  77. package/dist/types.js.map +1 -0
  78. package/package.json +48 -0
  79. package/src/api/license.ts +120 -0
  80. package/src/api/rules.ts +70 -0
  81. package/src/cli/commands/init.ts +123 -0
  82. package/src/cli/commands/login.ts +92 -0
  83. package/src/cli/commands/mcp.ts +12 -0
  84. package/src/cli/commands/rules.ts +58 -0
  85. package/src/cli/commands/scan.ts +94 -0
  86. package/src/cli/config.ts +54 -0
  87. package/src/cli/index.ts +28 -0
  88. package/src/cli/output.ts +159 -0
  89. package/src/mcp/server.ts +195 -0
  90. package/src/scanner/index.ts +195 -0
  91. package/src/scanner/parsers/javascript.ts +285 -0
  92. package/src/scanner/parsers/python.ts +126 -0
  93. package/src/scanner/rules/definitions.ts +592 -0
  94. package/src/scanner/rules/loader.ts +59 -0
  95. package/src/scanner/rules/matcher.ts +68 -0
  96. package/src/types.ts +36 -0
  97. package/test-samples/secure.js +52 -0
  98. package/test-samples/vulnerable.js +56 -0
  99. package/test-samples/vulnerable.py +39 -0
  100. package/tests/helpers.ts +43 -0
  101. package/tests/rules/critical.test.ts +186 -0
  102. package/tests/rules/definitions.test.ts +167 -0
  103. package/tests/rules/high.test.ts +377 -0
  104. package/tests/rules/low.test.ts +172 -0
  105. package/tests/rules/medium.test.ts +224 -0
  106. package/tests/scanner/scanner.test.ts +161 -0
  107. package/tsconfig.json +19 -0
  108. package/vibe-coding-security-checklist.md +245 -0
  109. package/vitest.config.ts +15 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loader.js","sourceRoot":"","sources":["../../../src/scanner/rules/loader.ts"],"names":[],"mappings":";;AAKA,8BA0BC;AAED,kCAyBC;AAzDD,+CAA8C;AAE9C,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,iBAAiB,IAAI,2BAA2B,CAAC;AAE3E,KAAK,UAAU,SAAS,CAAC,UAAmB;IACjD,2BAA2B;IAC3B,+CAA+C;IAC/C,IAAI,OAAO,CAAC,GAAG,CAAC,iBAAiB,KAAK,MAAM,IAAI,CAAC,UAAU,EAAE,CAAC;QAC5D,OAAO,2BAAa,CAAC;IACvB,CAAC;IAED,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,YAAY,WAAW,EAAE;YACvD,OAAO,EAAE;gBACP,eAAe,EAAE,UAAU,UAAU,EAAE;gBACvC,cAAc,EAAE,kBAAkB;aACnC;SACF,CAAC,CAAC;QAEH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;YACjB,wCAAwC;YACxC,OAAO,2BAAa,CAAC;QACvB,CAAC;QAED,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAA+B,CAAC;QACnE,OAAO,IAAI,CAAC,KAAK,IAAI,2BAAa,CAAC;IACrC,CAAC;IAAC,MAAM,CAAC;QACP,4CAA4C;QAC5C,OAAO,2BAAa,CAAC;IACvB,CAAC;AACH,CAAC;AAED,SAAgB,WAAW,CACzB,KAAqB,EACrB,OAIC;IAED,IAAI,QAAQ,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC;IAE1B,IAAI,OAAO,CAAC,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAClD,QAAQ,GAAG,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,OAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;IACzE,CAAC;IAED,IAAI,OAAO,CAAC,QAAQ,IAAI,OAAO,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACpD,QAAQ,GAAG,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,QAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;IAC3E,CAAC;IAED,IAAI,OAAO,CAAC,SAAS,IAAI,OAAO,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACtD,QAAQ,GAAG,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAChC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,SAAU,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAC/D,CAAC;IACJ,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC"}
@@ -0,0 +1,11 @@
1
+ import { SecurityRule, Finding } from '../../types';
2
+ export interface MatchContext {
3
+ code: string;
4
+ lines: string[];
5
+ filePath: string;
6
+ language: string;
7
+ }
8
+ export declare function matchPatterns(rule: SecurityRule, context: MatchContext): Finding[];
9
+ export declare function deduplicateFindings(findings: Finding[]): Finding[];
10
+ export declare function sortBySeverity(findings: Finding[]): Finding[];
11
+ //# sourceMappingURL=matcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"matcher.d.ts","sourceRoot":"","sources":["../../../src/scanner/rules/matcher.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAEpD,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,wBAAgB,aAAa,CAC3B,IAAI,EAAE,YAAY,EAClB,OAAO,EAAE,YAAY,GACpB,OAAO,EAAE,CAmCX;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,OAAO,EAAE,CAQlE;AAED,wBAAgB,cAAc,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,OAAO,EAAE,CAQ7D"}
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.matchPatterns = matchPatterns;
4
+ exports.deduplicateFindings = deduplicateFindings;
5
+ exports.sortBySeverity = sortBySeverity;
6
+ function matchPatterns(rule, context) {
7
+ const findings = [];
8
+ if (!rule.patterns)
9
+ return findings;
10
+ for (const pattern of rule.patterns) {
11
+ let match;
12
+ const regex = new RegExp(pattern.source, pattern.flags + (pattern.flags.includes('g') ? '' : 'g'));
13
+ while ((match = regex.exec(context.code)) !== null) {
14
+ const beforeMatch = context.code.substring(0, match.index);
15
+ const lineNumber = (beforeMatch.match(/\n/g) || []).length + 1;
16
+ const lineStart = beforeMatch.lastIndexOf('\n') + 1;
17
+ const column = match.index - lineStart;
18
+ findings.push({
19
+ rule,
20
+ file: context.filePath,
21
+ line: lineNumber,
22
+ column,
23
+ code: context.lines[lineNumber - 1] || '',
24
+ message: rule.description,
25
+ });
26
+ // Prevent infinite loops on zero-width matches
27
+ if (match.index === regex.lastIndex) {
28
+ regex.lastIndex++;
29
+ }
30
+ }
31
+ }
32
+ return findings;
33
+ }
34
+ function deduplicateFindings(findings) {
35
+ const seen = new Set();
36
+ return findings.filter(finding => {
37
+ const key = `${finding.rule.id}:${finding.file}:${finding.line}`;
38
+ if (seen.has(key))
39
+ return false;
40
+ seen.add(key);
41
+ return true;
42
+ });
43
+ }
44
+ function sortBySeverity(findings) {
45
+ const severityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
46
+ return findings.sort((a, b) => {
47
+ const severityDiff = severityOrder[a.rule.severity] - severityOrder[b.rule.severity];
48
+ if (severityDiff !== 0)
49
+ return severityDiff;
50
+ return a.file.localeCompare(b.file) || a.line - b.line;
51
+ });
52
+ }
53
+ //# sourceMappingURL=matcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"matcher.js","sourceRoot":"","sources":["../../../src/scanner/rules/matcher.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ export type Severity = 'critical' | 'high' | 'medium' | 'low';
2
+ export interface SecurityRule {
3
+ id: string;
4
+ name: string;
5
+ description: string;
6
+ severity: Severity;
7
+ languages: ('javascript' | 'typescript' | 'python')[];
8
+ patterns?: RegExp[];
9
+ astMatcher?: string;
10
+ fix?: string;
11
+ }
12
+ export interface Finding {
13
+ rule: SecurityRule;
14
+ file: string;
15
+ line: number;
16
+ column: number;
17
+ code: string;
18
+ message: string;
19
+ }
20
+ export interface ScanResult {
21
+ files: number;
22
+ findings: Finding[];
23
+ duration: number;
24
+ }
25
+ export interface Config {
26
+ exclude?: string[];
27
+ severity?: Severity;
28
+ rules?: {
29
+ enabled?: string[];
30
+ disabled?: string[];
31
+ };
32
+ }
33
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,QAAQ,GAAG,UAAU,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAC;AAE9D,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,QAAQ,CAAC;IACnB,SAAS,EAAE,CAAC,YAAY,GAAG,YAAY,GAAG,QAAQ,CAAC,EAAE,CAAC;IACtD,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,YAAY,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,MAAM;IACrB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,KAAK,CAAC,EAAE;QACN,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;QACnB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;KACrB,CAAC;CACH"}
package/dist/types.js ADDED
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@indicated/vibeguard",
3
+ "version": "1.0.0",
4
+ "description": "Local CLI security scanner for AI-generated code",
5
+ "main": "dist/cli/index.js",
6
+ "bin": {
7
+ "vibeguard": "./dist/cli/index.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "start": "node dist/cli/index.js",
12
+ "dev": "tsc --watch",
13
+ "test": "vitest run",
14
+ "test:watch": "vitest",
15
+ "test:coverage": "vitest run --coverage"
16
+ },
17
+ "keywords": [
18
+ "security",
19
+ "scanner",
20
+ "cli",
21
+ "vulnerability",
22
+ "code-analysis"
23
+ ],
24
+ "author": "",
25
+ "license": "ISC",
26
+ "type": "commonjs",
27
+ "engines": {
28
+ "node": ">=18.0.0"
29
+ },
30
+ "dependencies": {
31
+ "@babel/parser": "^7.28.6",
32
+ "@babel/traverse": "^7.28.6",
33
+ "@babel/types": "^7.28.6",
34
+ "@modelcontextprotocol/sdk": "^1.25.3",
35
+ "@types/node": "^25.1.0",
36
+ "chalk": "^5.6.2",
37
+ "commander": "^14.0.2",
38
+ "glob": "^13.0.0",
39
+ "ora": "^9.1.0",
40
+ "typescript": "^5.9.3",
41
+ "zod": "^4.3.6"
42
+ },
43
+ "devDependencies": {
44
+ "@types/babel__traverse": "^7.28.0",
45
+ "@vitest/coverage-v8": "^4.0.18",
46
+ "vitest": "^4.0.18"
47
+ }
48
+ }
@@ -0,0 +1,120 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import * as os from 'os';
4
+
5
+ const CONFIG_DIR = path.join(os.homedir(), '.vibeguard');
6
+ const LICENSE_FILE = path.join(CONFIG_DIR, 'license.json');
7
+ const API_BASE_URL = process.env.VIBEGUARD_API_URL || 'https://api.vibeguard.dev';
8
+
9
+ interface LicenseData {
10
+ key: string;
11
+ email?: string;
12
+ validUntil?: string;
13
+ }
14
+
15
+ export function getLicenseKey(): string | null {
16
+ try {
17
+ if (fs.existsSync(LICENSE_FILE)) {
18
+ const data = JSON.parse(fs.readFileSync(LICENSE_FILE, 'utf-8'));
19
+ return data.key || null;
20
+ }
21
+ } catch {
22
+ // Ignore errors
23
+ }
24
+ return null;
25
+ }
26
+
27
+ export function saveLicenseKey(key: string, email?: string): void {
28
+ if (!fs.existsSync(CONFIG_DIR)) {
29
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
30
+ }
31
+
32
+ const data: LicenseData = { key, email };
33
+ fs.writeFileSync(LICENSE_FILE, JSON.stringify(data, null, 2));
34
+ }
35
+
36
+ export function clearLicenseKey(): void {
37
+ if (fs.existsSync(LICENSE_FILE)) {
38
+ fs.unlinkSync(LICENSE_FILE);
39
+ }
40
+ }
41
+
42
+ export async function validateLicense(key: string): Promise<{
43
+ valid: boolean;
44
+ message?: string;
45
+ tier?: string;
46
+ }> {
47
+ // For offline/development mode, accept any key
48
+ if (process.env.VIBEGUARD_OFFLINE === 'true') {
49
+ return { valid: true, tier: 'offline' };
50
+ }
51
+
52
+ try {
53
+ const response = await fetch(`${API_BASE_URL}/v1/license/validate`, {
54
+ method: 'POST',
55
+ headers: {
56
+ 'Content-Type': 'application/json',
57
+ },
58
+ body: JSON.stringify({ key }),
59
+ });
60
+
61
+ if (response.ok) {
62
+ const data = (await response.json()) as { tier?: string };
63
+ return {
64
+ valid: true,
65
+ tier: data.tier || 'standard',
66
+ };
67
+ }
68
+
69
+ const error = (await response.json().catch(() => ({}))) as { message?: string };
70
+ return {
71
+ valid: false,
72
+ message: error.message || 'Invalid license key',
73
+ };
74
+ } catch {
75
+ // If API is unreachable, allow offline mode
76
+ return {
77
+ valid: true,
78
+ message: 'Running in offline mode',
79
+ tier: 'offline',
80
+ };
81
+ }
82
+ }
83
+
84
+ export async function activateLicense(
85
+ email: string,
86
+ key: string
87
+ ): Promise<{ success: boolean; message: string }> {
88
+ if (process.env.VIBEGUARD_OFFLINE === 'true') {
89
+ saveLicenseKey(key, email);
90
+ return { success: true, message: 'License activated in offline mode' };
91
+ }
92
+
93
+ try {
94
+ const response = await fetch(`${API_BASE_URL}/v1/license/activate`, {
95
+ method: 'POST',
96
+ headers: {
97
+ 'Content-Type': 'application/json',
98
+ },
99
+ body: JSON.stringify({ email, key }),
100
+ });
101
+
102
+ if (response.ok) {
103
+ saveLicenseKey(key, email);
104
+ return { success: true, message: 'License activated successfully' };
105
+ }
106
+
107
+ const error = (await response.json().catch(() => ({}))) as { message?: string };
108
+ return {
109
+ success: false,
110
+ message: error.message || 'Failed to activate license',
111
+ };
112
+ } catch {
113
+ // Allow offline activation
114
+ saveLicenseKey(key, email);
115
+ return {
116
+ success: true,
117
+ message: 'License saved locally (offline mode)',
118
+ };
119
+ }
120
+ }
@@ -0,0 +1,70 @@
1
+ import { SecurityRule } from '../types';
2
+ import { securityRules } from '../scanner/rules/definitions';
3
+
4
+ const API_BASE_URL = process.env.VIBEGUARD_API_URL || 'https://api.vibeguard.dev';
5
+
6
+ interface RulesResponse {
7
+ version: string;
8
+ rules: SecurityRule[];
9
+ }
10
+
11
+ export async function fetchLatestRules(licenseKey?: string): Promise<RulesResponse> {
12
+ // For offline mode or no license, use bundled rules
13
+ if (process.env.VIBEGUARD_OFFLINE === 'true' || !licenseKey) {
14
+ return {
15
+ version: 'local',
16
+ rules: securityRules,
17
+ };
18
+ }
19
+
20
+ try {
21
+ const response = await fetch(`${API_BASE_URL}/v1/rules`, {
22
+ headers: {
23
+ 'Authorization': `Bearer ${licenseKey}`,
24
+ 'Accept': 'application/json',
25
+ },
26
+ });
27
+
28
+ if (response.ok) {
29
+ return (await response.json()) as RulesResponse;
30
+ }
31
+
32
+ // Fall back to local rules
33
+ return {
34
+ version: 'local',
35
+ rules: securityRules,
36
+ };
37
+ } catch {
38
+ // Network error, use local rules
39
+ return {
40
+ version: 'local',
41
+ rules: securityRules,
42
+ };
43
+ }
44
+ }
45
+
46
+ export async function reportScanMetrics(
47
+ licenseKey: string,
48
+ metrics: {
49
+ filesScanned: number;
50
+ findingsCount: number;
51
+ duration: number;
52
+ }
53
+ ): Promise<void> {
54
+ if (process.env.VIBEGUARD_OFFLINE === 'true') {
55
+ return;
56
+ }
57
+
58
+ try {
59
+ await fetch(`${API_BASE_URL}/v1/metrics`, {
60
+ method: 'POST',
61
+ headers: {
62
+ 'Authorization': `Bearer ${licenseKey}`,
63
+ 'Content-Type': 'application/json',
64
+ },
65
+ body: JSON.stringify(metrics),
66
+ });
67
+ } catch {
68
+ // Silently ignore metrics errors
69
+ }
70
+ }
@@ -0,0 +1,123 @@
1
+ import { Command } from 'commander';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import { execSync } from 'child_process';
5
+ import { formatSuccess, formatError, formatInfo, formatWarning } from '../output';
6
+
7
+ const HUSKY_HOOK = `#!/usr/bin/env sh
8
+ . "$(dirname -- "$0")/_/husky.sh"
9
+
10
+ npx vibeguard scan --staged
11
+ `;
12
+
13
+ const SIMPLE_HOOK = `#!/bin/sh
14
+ npx vibeguard scan --staged
15
+ `;
16
+
17
+ export function createInitCommand(): Command {
18
+ const init = new Command('init')
19
+ .description('Set up pre-commit hook for automatic scanning')
20
+ .option('--force', 'Overwrite existing hooks')
21
+ .action(async (options) => {
22
+ const cwd = process.cwd();
23
+
24
+ try {
25
+ // Check if we're in a git repo
26
+ if (!fs.existsSync(path.join(cwd, '.git'))) {
27
+ console.log(formatError('Not a git repository. Run "git init" first.'));
28
+ process.exit(1);
29
+ }
30
+
31
+ // Check for husky
32
+ const packageJsonPath = path.join(cwd, 'package.json');
33
+ let hasHusky = false;
34
+ let huskyDir = '';
35
+
36
+ if (fs.existsSync(packageJsonPath)) {
37
+ const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
38
+ hasHusky = !!(
39
+ pkg.devDependencies?.husky ||
40
+ pkg.dependencies?.husky
41
+ );
42
+ }
43
+
44
+ // Check for .husky directory
45
+ const huskyPath = path.join(cwd, '.husky');
46
+ if (fs.existsSync(huskyPath)) {
47
+ hasHusky = true;
48
+ huskyDir = huskyPath;
49
+ }
50
+
51
+ if (hasHusky && huskyDir) {
52
+ // Add husky hook
53
+ const hookPath = path.join(huskyDir, 'pre-commit');
54
+
55
+ if (fs.existsSync(hookPath) && !options.force) {
56
+ // Check if vibeguard is already in the hook
57
+ const existingHook = fs.readFileSync(hookPath, 'utf-8');
58
+ if (existingHook.includes('vibeguard')) {
59
+ console.log(formatInfo('VibeGuard hook already installed.'));
60
+ return;
61
+ }
62
+
63
+ // Append to existing hook
64
+ const updatedHook = existingHook.trimEnd() + '\n\nnpx vibeguard scan --staged\n';
65
+ fs.writeFileSync(hookPath, updatedHook);
66
+ console.log(formatSuccess('Added VibeGuard to existing pre-commit hook.'));
67
+ } else {
68
+ fs.writeFileSync(hookPath, HUSKY_HOOK);
69
+ fs.chmodSync(hookPath, '755');
70
+ console.log(formatSuccess('Created pre-commit hook with VibeGuard.'));
71
+ }
72
+ } else {
73
+ // No husky, use simple git hook
74
+ const hooksDir = path.join(cwd, '.git', 'hooks');
75
+ const hookPath = path.join(hooksDir, 'pre-commit');
76
+
77
+ if (fs.existsSync(hookPath) && !options.force) {
78
+ const existingHook = fs.readFileSync(hookPath, 'utf-8');
79
+ if (existingHook.includes('vibeguard')) {
80
+ console.log(formatInfo('VibeGuard hook already installed.'));
81
+ return;
82
+ }
83
+
84
+ // Append to existing hook
85
+ const updatedHook = existingHook.trimEnd() + '\n\nnpx vibeguard scan --staged\n';
86
+ fs.writeFileSync(hookPath, updatedHook);
87
+ fs.chmodSync(hookPath, '755');
88
+ console.log(formatSuccess('Added VibeGuard to existing pre-commit hook.'));
89
+ } else {
90
+ fs.writeFileSync(hookPath, SIMPLE_HOOK);
91
+ fs.chmodSync(hookPath, '755');
92
+ console.log(formatSuccess('Created pre-commit hook.'));
93
+ }
94
+
95
+ console.log(formatInfo('Tip: Consider using Husky for better hook management.'));
96
+ console.log(formatInfo(' npm install husky --save-dev'));
97
+ console.log(formatInfo(' npx husky init'));
98
+ }
99
+
100
+ // Create .vibeguardrc.json if it doesn't exist
101
+ const configPath = path.join(cwd, '.vibeguardrc.json');
102
+ if (!fs.existsSync(configPath)) {
103
+ const defaultConfig = {
104
+ exclude: ['node_modules', 'dist', 'build'],
105
+ rules: {
106
+ disabled: [],
107
+ },
108
+ };
109
+ fs.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2));
110
+ console.log(formatSuccess('Created .vibeguardrc.json config file.'));
111
+ }
112
+
113
+ console.log('\n' + formatSuccess('VibeGuard initialized successfully!'));
114
+ console.log(formatInfo('Your code will be scanned before each commit.'));
115
+ console.log(formatInfo('Use "git commit --no-verify" to skip the check if needed.'));
116
+ } catch (error) {
117
+ console.error(formatError(error instanceof Error ? error.message : 'Init failed'));
118
+ process.exit(1);
119
+ }
120
+ });
121
+
122
+ return init;
123
+ }
@@ -0,0 +1,92 @@
1
+ import { Command } from 'commander';
2
+ import * as readline from 'readline';
3
+ import { activateLicense, clearLicenseKey, getLicenseKey } from '../../api/license';
4
+ import { formatSuccess, formatError, formatInfo } from '../output';
5
+
6
+ function prompt(question: string): Promise<string> {
7
+ const rl = readline.createInterface({
8
+ input: process.stdin,
9
+ output: process.stdout,
10
+ });
11
+
12
+ return new Promise(resolve => {
13
+ rl.question(question, answer => {
14
+ rl.close();
15
+ resolve(answer.trim());
16
+ });
17
+ });
18
+ }
19
+
20
+ export function createLoginCommand(): Command {
21
+ const login = new Command('login')
22
+ .description('Authenticate with your VibeGuard license key')
23
+ .option('--key <key>', 'License key (or enter interactively)')
24
+ .option('--email <email>', 'Email address associated with license')
25
+ .action(async (options) => {
26
+ try {
27
+ // Check if already logged in
28
+ const existingKey = getLicenseKey();
29
+ if (existingKey) {
30
+ console.log(formatInfo('You are already logged in.'));
31
+ console.log(formatInfo('Use "vibeguard logout" to log out first.'));
32
+ return;
33
+ }
34
+
35
+ // Get license key
36
+ let key = options.key;
37
+ if (!key) {
38
+ key = await prompt('Enter your license key: ');
39
+ }
40
+
41
+ if (!key) {
42
+ console.log(formatError('License key is required'));
43
+ process.exit(1);
44
+ }
45
+
46
+ // Get email
47
+ let email = options.email;
48
+ if (!email) {
49
+ email = await prompt('Enter your email (optional): ');
50
+ }
51
+
52
+ // Activate license
53
+ console.log('\nActivating license...');
54
+ const result = await activateLicense(email || '', key);
55
+
56
+ if (result.success) {
57
+ console.log(formatSuccess(result.message));
58
+ console.log(formatInfo('You can now run "vibeguard scan" to scan your code.'));
59
+ } else {
60
+ console.log(formatError(result.message));
61
+ process.exit(1);
62
+ }
63
+ } catch (error) {
64
+ console.error(formatError(error instanceof Error ? error.message : 'Login failed'));
65
+ process.exit(1);
66
+ }
67
+ });
68
+
69
+ return login;
70
+ }
71
+
72
+ export function createLogoutCommand(): Command {
73
+ const logout = new Command('logout')
74
+ .description('Remove stored license key')
75
+ .action(() => {
76
+ try {
77
+ const existingKey = getLicenseKey();
78
+ if (!existingKey) {
79
+ console.log(formatInfo('You are not logged in.'));
80
+ return;
81
+ }
82
+
83
+ clearLicenseKey();
84
+ console.log(formatSuccess('Logged out successfully.'));
85
+ } catch (error) {
86
+ console.error(formatError(error instanceof Error ? error.message : 'Logout failed'));
87
+ process.exit(1);
88
+ }
89
+ });
90
+
91
+ return logout;
92
+ }
@@ -0,0 +1,12 @@
1
+ import { Command } from 'commander';
2
+ import { startMcpServer } from '../../mcp/server';
3
+
4
+ export function createMcpCommand(): Command {
5
+ const mcp = new Command('mcp')
6
+ .description('Start VibeGuard as an MCP server for AI assistant integration')
7
+ .action(async () => {
8
+ await startMcpServer();
9
+ });
10
+
11
+ return mcp;
12
+ }
@@ -0,0 +1,58 @@
1
+ import { Command } from 'commander';
2
+ import { loadRules } from '../../scanner/rules/loader';
3
+ import { getLicenseKey } from '../../api/license';
4
+ import { formatRule, formatHeader, formatInfo } from '../output';
5
+
6
+ const packageJson = require('../../../package.json');
7
+
8
+ export function createRulesCommand(): Command {
9
+ const rules = new Command('rules')
10
+ .description('List all available security rules')
11
+ .option('--severity <level>', 'Filter by severity (critical, high, medium, low)')
12
+ .option('--language <lang>', 'Filter by language (javascript, typescript, python)')
13
+ .option('--json', 'Output as JSON')
14
+ .action(async (options) => {
15
+ try {
16
+ const licenseKey = getLicenseKey();
17
+ let allRules = await loadRules(licenseKey || undefined);
18
+
19
+ // Apply filters
20
+ if (options.severity) {
21
+ allRules = allRules.filter(r => r.severity === options.severity);
22
+ }
23
+
24
+ if (options.language) {
25
+ allRules = allRules.filter(r =>
26
+ r.languages.includes(options.language as 'javascript' | 'typescript' | 'python')
27
+ );
28
+ }
29
+
30
+ if (options.json) {
31
+ console.log(JSON.stringify(allRules, null, 2));
32
+ return;
33
+ }
34
+
35
+ console.log(formatHeader(packageJson.version));
36
+ console.log(formatInfo(`${allRules.length} security rules available\n`));
37
+
38
+ // Group by severity
39
+ const severities = ['critical', 'high', 'medium', 'low'] as const;
40
+
41
+ for (const severity of severities) {
42
+ const rulesInSeverity = allRules.filter(r => r.severity === severity);
43
+ if (rulesInSeverity.length > 0) {
44
+ console.log(`\n${severity.toUpperCase()} (${rulesInSeverity.length})`);
45
+ console.log('─'.repeat(40));
46
+ for (const rule of rulesInSeverity) {
47
+ console.log(formatRule(rule));
48
+ }
49
+ }
50
+ }
51
+ } catch (error) {
52
+ console.error('Failed to load rules:', error instanceof Error ? error.message : error);
53
+ process.exit(1);
54
+ }
55
+ });
56
+
57
+ return rules;
58
+ }