@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.
- package/.claude/settings.local.json +5 -0
- package/.github/workflows/ci.yml +65 -0
- package/.github/workflows/release.yml +85 -0
- package/PROGRESS.md +192 -0
- package/README.md +183 -0
- package/dist/api/license.d.ts +13 -0
- package/dist/api/license.d.ts.map +1 -0
- package/dist/api/license.js +138 -0
- package/dist/api/license.js.map +1 -0
- package/dist/api/rules.d.ts +13 -0
- package/dist/api/rules.d.ts.map +1 -0
- package/dist/api/rules.js +57 -0
- package/dist/api/rules.js.map +1 -0
- package/dist/cli/commands/init.d.ts +3 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +145 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/login.d.ts +4 -0
- package/dist/cli/commands/login.d.ts.map +1 -0
- package/dist/cli/commands/login.js +121 -0
- package/dist/cli/commands/login.js.map +1 -0
- package/dist/cli/commands/mcp.d.ts +3 -0
- package/dist/cli/commands/mcp.d.ts.map +1 -0
- package/dist/cli/commands/mcp.js +14 -0
- package/dist/cli/commands/mcp.js.map +1 -0
- package/dist/cli/commands/rules.d.ts +3 -0
- package/dist/cli/commands/rules.d.ts.map +1 -0
- package/dist/cli/commands/rules.js +52 -0
- package/dist/cli/commands/rules.js.map +1 -0
- package/dist/cli/commands/scan.d.ts +3 -0
- package/dist/cli/commands/scan.d.ts.map +1 -0
- package/dist/cli/commands/scan.js +114 -0
- package/dist/cli/commands/scan.js.map +1 -0
- package/dist/cli/config.d.ts +4 -0
- package/dist/cli/config.d.ts.map +1 -0
- package/dist/cli/config.js +88 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +25 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/output.d.ts +15 -0
- package/dist/cli/output.d.ts.map +1 -0
- package/dist/cli/output.js +152 -0
- package/dist/cli/output.js.map +1 -0
- package/dist/mcp/server.d.ts +2 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +188 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/scanner/index.d.ts +15 -0
- package/dist/scanner/index.d.ts.map +1 -0
- package/dist/scanner/index.js +207 -0
- package/dist/scanner/index.js.map +1 -0
- package/dist/scanner/parsers/javascript.d.ts +12 -0
- package/dist/scanner/parsers/javascript.d.ts.map +1 -0
- package/dist/scanner/parsers/javascript.js +266 -0
- package/dist/scanner/parsers/javascript.js.map +1 -0
- package/dist/scanner/parsers/python.d.ts +3 -0
- package/dist/scanner/parsers/python.d.ts.map +1 -0
- package/dist/scanner/parsers/python.js +108 -0
- package/dist/scanner/parsers/python.js.map +1 -0
- package/dist/scanner/rules/definitions.d.ts +5 -0
- package/dist/scanner/rules/definitions.d.ts.map +1 -0
- package/dist/scanner/rules/definitions.js +584 -0
- package/dist/scanner/rules/definitions.js.map +1 -0
- package/dist/scanner/rules/loader.d.ts +8 -0
- package/dist/scanner/rules/loader.d.ts.map +1 -0
- package/dist/scanner/rules/loader.js +45 -0
- package/dist/scanner/rules/loader.js.map +1 -0
- package/dist/scanner/rules/matcher.d.ts +11 -0
- package/dist/scanner/rules/matcher.d.ts.map +1 -0
- package/dist/scanner/rules/matcher.js +53 -0
- package/dist/scanner/rules/matcher.js.map +1 -0
- package/dist/types.d.ts +33 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +48 -0
- package/src/api/license.ts +120 -0
- package/src/api/rules.ts +70 -0
- package/src/cli/commands/init.ts +123 -0
- package/src/cli/commands/login.ts +92 -0
- package/src/cli/commands/mcp.ts +12 -0
- package/src/cli/commands/rules.ts +58 -0
- package/src/cli/commands/scan.ts +94 -0
- package/src/cli/config.ts +54 -0
- package/src/cli/index.ts +28 -0
- package/src/cli/output.ts +159 -0
- package/src/mcp/server.ts +195 -0
- package/src/scanner/index.ts +195 -0
- package/src/scanner/parsers/javascript.ts +285 -0
- package/src/scanner/parsers/python.ts +126 -0
- package/src/scanner/rules/definitions.ts +592 -0
- package/src/scanner/rules/loader.ts +59 -0
- package/src/scanner/rules/matcher.ts +68 -0
- package/src/types.ts +36 -0
- package/test-samples/secure.js +52 -0
- package/test-samples/vulnerable.js +56 -0
- package/test-samples/vulnerable.py +39 -0
- package/tests/helpers.ts +43 -0
- package/tests/rules/critical.test.ts +186 -0
- package/tests/rules/definitions.test.ts +167 -0
- package/tests/rules/high.test.ts +377 -0
- package/tests/rules/low.test.ts +172 -0
- package/tests/rules/medium.test.ts +224 -0
- package/tests/scanner/scanner.test.ts +161 -0
- package/tsconfig.json +19 -0
- package/vibe-coding-security-checklist.md +245 -0
- package/vitest.config.ts +15 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { Scanner } from '../../scanner';
|
|
4
|
+
import { getLicenseKey } from '../../api/license';
|
|
5
|
+
import { loadConfig } from '../config';
|
|
6
|
+
import {
|
|
7
|
+
formatHeader,
|
|
8
|
+
formatScanning,
|
|
9
|
+
formatFinding,
|
|
10
|
+
formatSummary,
|
|
11
|
+
formatBlockedCommit,
|
|
12
|
+
formatCleanResult,
|
|
13
|
+
formatError,
|
|
14
|
+
shouldBlockCommit,
|
|
15
|
+
} from '../output';
|
|
16
|
+
|
|
17
|
+
const packageJson = require('../../../package.json');
|
|
18
|
+
|
|
19
|
+
export function createScanCommand(): Command {
|
|
20
|
+
const scan = new Command('scan')
|
|
21
|
+
.description('Scan files or directories for security vulnerabilities')
|
|
22
|
+
.argument('[targets...]', 'Files or directories to scan', ['.'])
|
|
23
|
+
.option('--staged', 'Scan only git staged files')
|
|
24
|
+
.option('--force', 'Continue even if critical/high issues found')
|
|
25
|
+
.option('--json', 'Output results as JSON')
|
|
26
|
+
.option('--quiet', 'Minimal output (exit code only)')
|
|
27
|
+
.action(async (targets: string[], options) => {
|
|
28
|
+
try {
|
|
29
|
+
const config = loadConfig();
|
|
30
|
+
const licenseKey = getLicenseKey();
|
|
31
|
+
const cwd = process.cwd();
|
|
32
|
+
|
|
33
|
+
const scanner = new Scanner(config);
|
|
34
|
+
await scanner.initialize(licenseKey || undefined);
|
|
35
|
+
|
|
36
|
+
if (!options.quiet && !options.json) {
|
|
37
|
+
console.log(formatHeader(packageJson.version));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Perform scan
|
|
41
|
+
const result = options.staged
|
|
42
|
+
? await scanner.scanStaged()
|
|
43
|
+
: await scanner.scan(targets.length > 0 ? targets : ['.']);
|
|
44
|
+
|
|
45
|
+
if (!options.quiet && !options.json) {
|
|
46
|
+
console.log(formatScanning(result.files));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Output results
|
|
50
|
+
if (options.json) {
|
|
51
|
+
console.log(JSON.stringify({
|
|
52
|
+
version: packageJson.version,
|
|
53
|
+
files: result.files,
|
|
54
|
+
findings: result.findings.map(f => ({
|
|
55
|
+
rule: f.rule.id,
|
|
56
|
+
severity: f.rule.severity,
|
|
57
|
+
file: path.relative(cwd, f.file),
|
|
58
|
+
line: f.line,
|
|
59
|
+
column: f.column,
|
|
60
|
+
message: f.rule.name,
|
|
61
|
+
fix: f.rule.fix,
|
|
62
|
+
})),
|
|
63
|
+
duration: result.duration,
|
|
64
|
+
}, null, 2));
|
|
65
|
+
} else if (!options.quiet) {
|
|
66
|
+
if (result.findings.length === 0) {
|
|
67
|
+
console.log(formatCleanResult());
|
|
68
|
+
} else {
|
|
69
|
+
for (const finding of result.findings) {
|
|
70
|
+
console.log(formatFinding(finding, cwd));
|
|
71
|
+
}
|
|
72
|
+
console.log(formatSummary(result));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Determine exit code
|
|
77
|
+
const hasBlockingIssues = shouldBlockCommit(result);
|
|
78
|
+
|
|
79
|
+
if (hasBlockingIssues && !options.force) {
|
|
80
|
+
if (!options.quiet && !options.json) {
|
|
81
|
+
console.log(formatBlockedCommit());
|
|
82
|
+
}
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
process.exit(0);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
console.error(formatError(error instanceof Error ? error.message : 'Scan failed'));
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return scan;
|
|
94
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { Config } from '../types';
|
|
4
|
+
|
|
5
|
+
const CONFIG_FILES = [
|
|
6
|
+
'.vibeguardrc.json',
|
|
7
|
+
'.vibeguardrc',
|
|
8
|
+
'vibeguard.config.json',
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
export function loadConfig(cwd: string = process.cwd()): Config {
|
|
12
|
+
for (const configFile of CONFIG_FILES) {
|
|
13
|
+
const configPath = path.join(cwd, configFile);
|
|
14
|
+
if (fs.existsSync(configPath)) {
|
|
15
|
+
try {
|
|
16
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
17
|
+
return JSON.parse(content) as Config;
|
|
18
|
+
} catch {
|
|
19
|
+
// Invalid config, continue to next
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Check package.json for vibeguard key
|
|
25
|
+
const packageJsonPath = path.join(cwd, 'package.json');
|
|
26
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
27
|
+
try {
|
|
28
|
+
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
29
|
+
if (pkg.vibeguard) {
|
|
30
|
+
return pkg.vibeguard as Config;
|
|
31
|
+
}
|
|
32
|
+
} catch {
|
|
33
|
+
// Invalid package.json
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Return default config
|
|
38
|
+
return {};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function createDefaultConfig(): Config {
|
|
42
|
+
return {
|
|
43
|
+
exclude: [
|
|
44
|
+
'node_modules',
|
|
45
|
+
'dist',
|
|
46
|
+
'build',
|
|
47
|
+
'.git',
|
|
48
|
+
'coverage',
|
|
49
|
+
],
|
|
50
|
+
rules: {
|
|
51
|
+
disabled: [],
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { createScanCommand } from './commands/scan';
|
|
5
|
+
import { createLoginCommand, createLogoutCommand } from './commands/login';
|
|
6
|
+
import { createInitCommand } from './commands/init';
|
|
7
|
+
import { createRulesCommand } from './commands/rules';
|
|
8
|
+
import { createMcpCommand } from './commands/mcp';
|
|
9
|
+
|
|
10
|
+
const packageJson = require('../../package.json');
|
|
11
|
+
|
|
12
|
+
const program = new Command();
|
|
13
|
+
|
|
14
|
+
program
|
|
15
|
+
.name('vibeguard')
|
|
16
|
+
.description('Local CLI security scanner for AI-generated code')
|
|
17
|
+
.version(packageJson.version);
|
|
18
|
+
|
|
19
|
+
// Add commands
|
|
20
|
+
program.addCommand(createScanCommand());
|
|
21
|
+
program.addCommand(createLoginCommand());
|
|
22
|
+
program.addCommand(createLogoutCommand());
|
|
23
|
+
program.addCommand(createInitCommand());
|
|
24
|
+
program.addCommand(createRulesCommand());
|
|
25
|
+
program.addCommand(createMcpCommand());
|
|
26
|
+
|
|
27
|
+
// Parse and execute
|
|
28
|
+
program.parse();
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { Finding, ScanResult, Severity, SecurityRule } from '../types';
|
|
2
|
+
|
|
3
|
+
// ANSI color codes (chalk is ESM-only, so we use direct codes for CommonJS compatibility)
|
|
4
|
+
const colors = {
|
|
5
|
+
reset: '\x1b[0m',
|
|
6
|
+
bold: '\x1b[1m',
|
|
7
|
+
dim: '\x1b[2m',
|
|
8
|
+
red: '\x1b[31m',
|
|
9
|
+
green: '\x1b[32m',
|
|
10
|
+
yellow: '\x1b[33m',
|
|
11
|
+
blue: '\x1b[34m',
|
|
12
|
+
magenta: '\x1b[35m',
|
|
13
|
+
cyan: '\x1b[36m',
|
|
14
|
+
white: '\x1b[37m',
|
|
15
|
+
bgRed: '\x1b[41m',
|
|
16
|
+
bgYellow: '\x1b[43m',
|
|
17
|
+
bgBlue: '\x1b[44m',
|
|
18
|
+
bgMagenta: '\x1b[45m',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const severityColors: Record<Severity, string> = {
|
|
22
|
+
critical: colors.bgRed + colors.white,
|
|
23
|
+
high: colors.red,
|
|
24
|
+
medium: colors.yellow,
|
|
25
|
+
low: colors.blue,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const severityLabels: Record<Severity, string> = {
|
|
29
|
+
critical: 'CRITICAL',
|
|
30
|
+
high: 'HIGH',
|
|
31
|
+
medium: 'MEDIUM',
|
|
32
|
+
low: 'LOW',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export function formatSeverity(severity: Severity): string {
|
|
36
|
+
const color = severityColors[severity];
|
|
37
|
+
const label = severityLabels[severity].padEnd(8);
|
|
38
|
+
return `${color}${colors.bold} ${label} ${colors.reset}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function formatFinding(finding: Finding, cwd: string): string {
|
|
42
|
+
const relativePath = finding.file.replace(cwd + '/', '');
|
|
43
|
+
const location = `${relativePath}:${finding.line}`;
|
|
44
|
+
const severity = formatSeverity(finding.rule.severity);
|
|
45
|
+
|
|
46
|
+
let output = `\n${severity} ${colors.cyan}${location}${colors.reset}\n`;
|
|
47
|
+
output += ` ${finding.rule.name}\n`;
|
|
48
|
+
|
|
49
|
+
if (finding.rule.fix) {
|
|
50
|
+
output += ` ${colors.dim}→ ${finding.rule.fix}${colors.reset}\n`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return output;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function formatSummary(result: ScanResult): string {
|
|
57
|
+
const counts = {
|
|
58
|
+
critical: 0,
|
|
59
|
+
high: 0,
|
|
60
|
+
medium: 0,
|
|
61
|
+
low: 0,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
for (const finding of result.findings) {
|
|
65
|
+
counts[finding.rule.severity]++;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const total = result.findings.length;
|
|
69
|
+
const grade = calculateGrade(counts);
|
|
70
|
+
|
|
71
|
+
let output = '\n';
|
|
72
|
+
output += `${colors.dim}─────────────────────────────────────────${colors.reset}\n`;
|
|
73
|
+
output += `Found ${colors.bold}${total}${colors.reset} issue${total !== 1 ? 's' : ''} `;
|
|
74
|
+
output += `(${colors.red}${counts.critical} critical${colors.reset}, `;
|
|
75
|
+
output += `${colors.yellow}${counts.high} high${colors.reset}, `;
|
|
76
|
+
output += `${colors.blue}${counts.medium} medium${colors.reset}, `;
|
|
77
|
+
output += `${colors.dim}${counts.low} low${colors.reset})\n\n`;
|
|
78
|
+
|
|
79
|
+
output += `Grade: ${formatGrade(grade)}\n`;
|
|
80
|
+
|
|
81
|
+
return output;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function calculateGrade(counts: Record<Severity, number>): string {
|
|
85
|
+
if (counts.critical > 0) return 'F';
|
|
86
|
+
if (counts.high > 2) return 'D';
|
|
87
|
+
if (counts.high > 0) return 'C';
|
|
88
|
+
if (counts.medium > 3) return 'C';
|
|
89
|
+
if (counts.medium > 0) return 'B';
|
|
90
|
+
if (counts.low > 5) return 'B';
|
|
91
|
+
if (counts.low > 0) return 'A';
|
|
92
|
+
return 'A+';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function formatGrade(grade: string): string {
|
|
96
|
+
const gradeColors: Record<string, string> = {
|
|
97
|
+
'A+': colors.green + colors.bold,
|
|
98
|
+
'A': colors.green,
|
|
99
|
+
'B': colors.blue,
|
|
100
|
+
'C': colors.yellow,
|
|
101
|
+
'D': colors.red,
|
|
102
|
+
'F': colors.bgRed + colors.white + colors.bold,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const color = gradeColors[grade] || colors.white;
|
|
106
|
+
return `${color}${grade}${colors.reset}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function formatHeader(version: string): string {
|
|
110
|
+
return `\n${colors.cyan}${colors.bold}VibeGuard${colors.reset} Security Scanner ${colors.dim}v${version}${colors.reset}\n`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function formatScanning(fileCount: number): string {
|
|
114
|
+
return `\n${colors.dim}Scanning ${fileCount} file${fileCount !== 1 ? 's' : ''}...${colors.reset}\n`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function formatSuccess(message: string): string {
|
|
118
|
+
return `${colors.green}✓${colors.reset} ${message}`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function formatError(message: string): string {
|
|
122
|
+
return `${colors.red}✗${colors.reset} ${message}`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function formatWarning(message: string): string {
|
|
126
|
+
return `${colors.yellow}⚠${colors.reset} ${message}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function formatInfo(message: string): string {
|
|
130
|
+
return `${colors.blue}ℹ${colors.reset} ${message}`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function formatRule(rule: SecurityRule): string {
|
|
134
|
+
const severity = formatSeverity(rule.severity);
|
|
135
|
+
let output = `${severity} ${colors.bold}${rule.id}${colors.reset}\n`;
|
|
136
|
+
output += ` ${rule.name}\n`;
|
|
137
|
+
output += ` ${colors.dim}${rule.description}${colors.reset}\n`;
|
|
138
|
+
if (rule.fix) {
|
|
139
|
+
output += ` ${colors.cyan}Fix: ${rule.fix}${colors.reset}\n`;
|
|
140
|
+
}
|
|
141
|
+
output += ` ${colors.dim}Languages: ${rule.languages.join(', ')}${colors.reset}\n`;
|
|
142
|
+
return output;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function formatBlockedCommit(): string {
|
|
146
|
+
return `\n${colors.bgRed}${colors.white}${colors.bold} COMMIT BLOCKED ${colors.reset}\n` +
|
|
147
|
+
`${colors.red}Fix critical/high issues or use ${colors.bold}git commit --no-verify${colors.reset}${colors.red} to override.${colors.reset}\n`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function formatCleanResult(): string {
|
|
151
|
+
return `\n${colors.green}${colors.bold}✓ No security issues found!${colors.reset}\n` +
|
|
152
|
+
`\nGrade: ${formatGrade('A+')}\n`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function shouldBlockCommit(result: ScanResult): boolean {
|
|
156
|
+
return result.findings.some(
|
|
157
|
+
f => f.rule.severity === 'critical' || f.rule.severity === 'high'
|
|
158
|
+
);
|
|
159
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import { Scanner } from '../scanner';
|
|
6
|
+
import { securityRules } from '../scanner/rules/definitions';
|
|
7
|
+
|
|
8
|
+
export async function startMcpServer(): Promise<void> {
|
|
9
|
+
const server = new McpServer({
|
|
10
|
+
name: 'vibeguard',
|
|
11
|
+
version: '1.0.0',
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
// Tool: scan_code
|
|
15
|
+
server.tool(
|
|
16
|
+
'scan_code',
|
|
17
|
+
'Scan files or directories for security vulnerabilities. Returns findings with severity, location, and fix suggestions. Use this after writing code or before commits.',
|
|
18
|
+
{
|
|
19
|
+
paths: z.array(z.string()).describe('File or directory paths to scan (relative to current working directory)'),
|
|
20
|
+
staged_only: z.boolean().optional().describe('If true, only scan git staged files'),
|
|
21
|
+
},
|
|
22
|
+
async ({ paths, staged_only }) => {
|
|
23
|
+
try {
|
|
24
|
+
const scanner = new Scanner();
|
|
25
|
+
await scanner.initialize();
|
|
26
|
+
|
|
27
|
+
const cwd = process.cwd();
|
|
28
|
+
const targets = paths.map(p => path.resolve(cwd, p));
|
|
29
|
+
|
|
30
|
+
const result = staged_only
|
|
31
|
+
? await scanner.scanStaged()
|
|
32
|
+
: await scanner.scan(targets);
|
|
33
|
+
|
|
34
|
+
if (result.findings.length === 0) {
|
|
35
|
+
return {
|
|
36
|
+
content: [
|
|
37
|
+
{
|
|
38
|
+
type: 'text' as const,
|
|
39
|
+
text: `✅ No security issues found in ${result.files} file(s).`,
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Format findings
|
|
46
|
+
const findings = result.findings.map(f => ({
|
|
47
|
+
severity: f.rule.severity,
|
|
48
|
+
rule: f.rule.id,
|
|
49
|
+
name: f.rule.name,
|
|
50
|
+
file: path.relative(cwd, f.file),
|
|
51
|
+
line: f.line,
|
|
52
|
+
message: f.rule.description,
|
|
53
|
+
fix: f.rule.fix,
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
const counts = {
|
|
57
|
+
critical: findings.filter(f => f.severity === 'critical').length,
|
|
58
|
+
high: findings.filter(f => f.severity === 'high').length,
|
|
59
|
+
medium: findings.filter(f => f.severity === 'medium').length,
|
|
60
|
+
low: findings.filter(f => f.severity === 'low').length,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const summary = `Found ${findings.length} issue(s): ${counts.critical} critical, ${counts.high} high, ${counts.medium} medium, ${counts.low} low`;
|
|
64
|
+
|
|
65
|
+
const formattedFindings = findings.map(f =>
|
|
66
|
+
`[${f.severity.toUpperCase()}] ${f.file}:${f.line}\n ${f.name}\n Fix: ${f.fix}`
|
|
67
|
+
).join('\n\n');
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
content: [
|
|
71
|
+
{
|
|
72
|
+
type: 'text' as const,
|
|
73
|
+
text: `${summary}\n\n${formattedFindings}`,
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
};
|
|
77
|
+
} catch (error) {
|
|
78
|
+
return {
|
|
79
|
+
content: [
|
|
80
|
+
{
|
|
81
|
+
type: 'text' as const,
|
|
82
|
+
text: `Error scanning: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
isError: true,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
// Tool: list_security_rules
|
|
92
|
+
server.tool(
|
|
93
|
+
'list_security_rules',
|
|
94
|
+
'List all available security rules that VibeGuard checks for. Use this to understand what vulnerabilities are detected.',
|
|
95
|
+
{
|
|
96
|
+
severity: z.enum(['critical', 'high', 'medium', 'low']).optional().describe('Filter by severity level'),
|
|
97
|
+
},
|
|
98
|
+
async ({ severity }) => {
|
|
99
|
+
let rules = securityRules;
|
|
100
|
+
|
|
101
|
+
if (severity) {
|
|
102
|
+
rules = rules.filter(r => r.severity === severity);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const formatted = rules.map(r =>
|
|
106
|
+
`[${r.severity.toUpperCase()}] ${r.id}\n ${r.name}\n ${r.description}\n Languages: ${r.languages.join(', ')}`
|
|
107
|
+
).join('\n\n');
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
content: [
|
|
111
|
+
{
|
|
112
|
+
type: 'text' as const,
|
|
113
|
+
text: `${rules.length} security rule(s):\n\n${formatted}`,
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
// Tool: check_code_snippet
|
|
121
|
+
server.tool(
|
|
122
|
+
'check_code_snippet',
|
|
123
|
+
'Check a code snippet for security vulnerabilities without writing to disk. Useful for validating code before suggesting it.',
|
|
124
|
+
{
|
|
125
|
+
code: z.string().describe('The code snippet to check'),
|
|
126
|
+
language: z.enum(['javascript', 'typescript', 'python']).describe('The programming language'),
|
|
127
|
+
},
|
|
128
|
+
async ({ code, language }) => {
|
|
129
|
+
try {
|
|
130
|
+
const fs = await import('fs');
|
|
131
|
+
const os = await import('os');
|
|
132
|
+
|
|
133
|
+
// Create temp file
|
|
134
|
+
const ext = language === 'python' ? '.py' : language === 'typescript' ? '.ts' : '.js';
|
|
135
|
+
const tempFile = path.join(os.tmpdir(), `vibeguard-check-${Date.now()}${ext}`);
|
|
136
|
+
|
|
137
|
+
fs.writeFileSync(tempFile, code);
|
|
138
|
+
|
|
139
|
+
const scanner = new Scanner();
|
|
140
|
+
await scanner.initialize();
|
|
141
|
+
|
|
142
|
+
const result = await scanner.scan([tempFile]);
|
|
143
|
+
|
|
144
|
+
// Clean up
|
|
145
|
+
fs.unlinkSync(tempFile);
|
|
146
|
+
|
|
147
|
+
if (result.findings.length === 0) {
|
|
148
|
+
return {
|
|
149
|
+
content: [
|
|
150
|
+
{
|
|
151
|
+
type: 'text' as const,
|
|
152
|
+
text: '✅ No security issues found in this code snippet.',
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const findings = result.findings.map(f => ({
|
|
159
|
+
severity: f.rule.severity,
|
|
160
|
+
rule: f.rule.id,
|
|
161
|
+
name: f.rule.name,
|
|
162
|
+
line: f.line,
|
|
163
|
+
fix: f.rule.fix,
|
|
164
|
+
}));
|
|
165
|
+
|
|
166
|
+
const formatted = findings.map(f =>
|
|
167
|
+
`[${f.severity.toUpperCase()}] Line ${f.line}: ${f.name}\n Fix: ${f.fix}`
|
|
168
|
+
).join('\n\n');
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
content: [
|
|
172
|
+
{
|
|
173
|
+
type: 'text' as const,
|
|
174
|
+
text: `Found ${findings.length} issue(s):\n\n${formatted}`,
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
};
|
|
178
|
+
} catch (error) {
|
|
179
|
+
return {
|
|
180
|
+
content: [
|
|
181
|
+
{
|
|
182
|
+
type: 'text' as const,
|
|
183
|
+
text: `Error checking code: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
isError: true,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
// Connect via stdio
|
|
193
|
+
const transport = new StdioServerTransport();
|
|
194
|
+
await server.connect(transport);
|
|
195
|
+
}
|