vibe-coder-mcp
Version:
Production-ready MCP server with complete agent integration, multi-transport support, and comprehensive development automation tools for AI-assisted workflows.
528 lines (527 loc) • 22.2 kB
JavaScript
import { exec } from 'child_process';
import { promisify } from 'util';
import * as fs from 'fs';
import * as path from 'path';
import logger from '../../../logger.js';
import { UnifiedSecurityConfigManager } from '../../vibe-task-manager/security/unified-security-config.js';
import { SemgrepRuleGenerator } from './semgrepRuleGenerator.js';
import { resolveImport } from '../utils/importResolver.js';
const execAsync = promisify(exec);
export class SemgrepAdapter {
allowedDir;
outputDir;
securityManager;
ruleGenerator;
cache = new Map();
tempFiles = [];
constructor(allowedDir, outputDir) {
this.allowedDir = allowedDir;
this.outputDir = outputDir;
this.securityManager = UnifiedSecurityConfigManager.getInstance();
this.ruleGenerator = new SemgrepRuleGenerator();
}
async analyzeImports(filePath, options) {
try {
const cacheKey = `${filePath}:${JSON.stringify(options)}`;
if (this.cache.has(cacheKey)) {
return this.cache.get(cacheKey);
}
const validation = this.securityManager.validatePathSecurity(filePath, { operation: 'read' });
if (!validation.isValid) {
logger.warn({ filePath, error: validation.error }, 'File path validation failed');
return [];
}
const rules = this.ruleGenerator.generateImportRules();
const rulesFile = path.join(this.outputDir, `semgrep-rules-${Date.now()}.yaml`);
this.tempFiles.push(rulesFile);
await this.ruleGenerator.writeRulesToFile(rules, rulesFile);
const command = this.buildSemgrepCommand(filePath, rulesFile, options);
const { stdout } = await execAsync(command);
const result = JSON.parse(stdout);
const imports = this.convertToImportInfo(result, filePath, options);
await fs.promises.unlink(rulesFile);
const fileIndex = this.tempFiles.indexOf(rulesFile);
if (fileIndex !== -1) {
this.tempFiles.splice(fileIndex, 1);
}
this.cache.set(cacheKey, imports);
return imports;
}
catch (error) {
logger.error({ err: error, filePath }, 'Error analyzing imports with Semgrep');
return [];
}
}
buildSemgrepCommand(filePath, rulesFile, options) {
const baseCommand = 'npx @semgrep/semgrep';
let command = `${baseCommand} --json --config ${rulesFile}`;
if (options.timeout) {
command += ` --timeout ${options.timeout}`;
}
if (options.maxMemory) {
command += ` --max-memory ${options.maxMemory}`;
}
if (options.excludePatterns && options.excludePatterns.length > 0) {
options.excludePatterns.forEach(pattern => {
command += ` --exclude ${pattern}`;
});
}
command += ` "${filePath}"`;
return command;
}
convertToImportInfo(result, filePath, options) {
const imports = [];
const importMap = new Map();
try {
if (!result.results || !Array.isArray(result.results)) {
return imports;
}
for (const match of result.results) {
try {
const importPath = this.extractImportPath(match);
if (!importPath) {
continue;
}
const resolvedPath = this.resolveImportPath(importPath, filePath, options.projectRoot || this.allowedDir);
const importInfo = this.createImportInfo(match, importPath, resolvedPath);
const key = importInfo.path;
if (importMap.has(key)) {
const existing = importMap.get(key);
if (importInfo.importedItems && importInfo.importedItems.length > 0) {
if (!existing.importedItems) {
existing.importedItems = [];
}
for (const item of importInfo.importedItems) {
if (!existing.importedItems.some(i => i.name === item.name)) {
existing.importedItems.push(item);
}
}
}
existing.metadata = {
...existing.metadata,
...importInfo.metadata
};
}
else {
importMap.set(key, importInfo);
}
}
catch (error) {
logger.warn({ err: error, match }, 'Error processing Semgrep match');
}
}
return Array.from(importMap.values());
}
catch (error) {
logger.error({ err: error, filePath }, 'Error converting Semgrep results to ImportInfo');
return imports;
}
}
extractImportPath(match) {
try {
const text = match.extra.lines;
if (match.check_id.startsWith('js-')) {
const regex = /(from|require)\s*['"](.*?)['"]/;
const match = text.match(regex);
return match ? match[2] : null;
}
else if (match.check_id.startsWith('python-')) {
if (match.check_id === 'python-from-import') {
const regex = /from\s+(.*?)\s+import/;
const match = text.match(regex);
return match ? match[1] : null;
}
else {
const regex = /import\s+(.*?)($|\s+as)/;
const match = text.match(regex);
return match ? match[1] : null;
}
}
else if (match.check_id.startsWith('java-')) {
const regex = /import\s+(?:static\s+)?(.*?)(?:\.\w+)?;/;
const match = text.match(regex);
return match ? match[1] : null;
}
else if (match.check_id.startsWith('cpp-')) {
const regex = /#include\s*[<"](.*?)[>"]/;
const match = text.match(regex);
return match ? match[1] : null;
}
else if (match.check_id.startsWith('ruby-')) {
const regex = /(?:require|require_relative|load)\s*['"](.*?)['"]/;
const match = text.match(regex);
return match ? match[1] : null;
}
else if (match.check_id.startsWith('go-')) {
const regex = /import\s+(?:\w+\s+)?["]([^"]+)["]/;
const match = text.match(regex);
return match ? match[1] : null;
}
else if (match.check_id.startsWith('php-')) {
if (match.check_id.includes('use')) {
const regex = /use\s+(.*?)(?:\s+as\s+|;)/;
const match = text.match(regex);
return match ? match[1] : null;
}
else {
const regex = /(?:require|include)(?:_once)?\s*['"](.*?)['"]/;
const match = text.match(regex);
return match ? match[1] : null;
}
}
return null;
}
catch (error) {
logger.warn({ err: error, match }, 'Error extracting import path from Semgrep match');
return null;
}
}
resolveImportPath(importPath, filePath, projectRoot) {
try {
const resolved = resolveImport(importPath, {
projectRoot,
fromFile: filePath,
language: path.extname(filePath).slice(1),
expandSecurityBoundary: true
});
return resolved;
}
catch (error) {
logger.debug({ err: error, importPath, filePath }, 'Error resolving import path');
return importPath;
}
}
createImportInfo(match, importPath, resolvedPath) {
const metadata = match.extra.metadata || {};
const isCore = metadata.isCore || this.isCorePath(importPath, match.check_id);
const isExternalPackage = !isCore && !metadata.isRelative && !resolvedPath.includes(this.allowedDir);
const importedItems = this.extractImportedItems(match);
const importInfo = {
path: resolvedPath || importPath,
importedItems,
isCore,
isExternalPackage,
isDynamic: metadata.isDynamic || false,
moduleSystem: metadata.moduleSystem || this.getModuleSystem(match.check_id),
metadata: {
originalPath: importPath,
importType: metadata.importType || match.check_id,
isRelative: metadata.isRelative || importPath.startsWith('.'),
matchedPattern: match.extra.lines
}
};
return importInfo;
}
extractImportedItems(match) {
const items = [];
try {
const text = match.extra.lines;
if (match.check_id.startsWith('js-')) {
if (match.check_id === 'js-import-default') {
const regex = /import\s+(\w+)\s+from/;
const m = text.match(regex);
if (m) {
items.push({
name: m[1],
isDefault: true,
isNamespace: false,
nodeText: text
});
}
}
else if (match.check_id === 'js-import-named') {
const regex = /import\s+{(.*?)}\s+from/;
const m = text.match(regex);
if (m) {
const namedImports = m[1].split(',').map(s => s.trim());
for (const namedImport of namedImports) {
const [name, alias] = namedImport.split(' as ').map(s => s.trim());
items.push({
name: alias || name,
alias: alias ? name : undefined,
isDefault: false,
isNamespace: false,
nodeText: text
});
}
}
}
else if (match.check_id === 'js-import-namespace') {
const regex = /import\s+\*\s+as\s+(\w+)\s+from/;
const m = text.match(regex);
if (m) {
items.push({
name: m[1],
isDefault: false,
isNamespace: true,
nodeText: text
});
}
}
}
else if (match.check_id.startsWith('python-')) {
if (match.check_id === 'python-import') {
const regex = /import\s+(\w+)/;
const m = text.match(regex);
if (m) {
items.push({
name: m[1],
isDefault: false,
isNamespace: false,
nodeText: text
});
}
}
else if (match.check_id === 'python-from-import') {
const regex = /from\s+.*?\s+import\s+(.*?)$/;
const m = text.match(regex);
if (m) {
const namedImports = m[1].split(',').map(s => s.trim());
for (const namedImport of namedImports) {
const [name, alias] = namedImport.split(' as ').map(s => s.trim());
items.push({
name: alias || name,
alias: alias ? name : undefined,
isDefault: false,
isNamespace: false,
nodeText: text
});
}
}
}
else if (match.check_id === 'python-import-as') {
const regex = /import\s+(\w+)\s+as\s+(\w+)/;
const m = text.match(regex);
if (m) {
items.push({
name: m[2],
alias: m[1],
isDefault: false,
isNamespace: false,
nodeText: text
});
}
}
}
else if (match.check_id.startsWith('java-')) {
if (match.check_id === 'java-import') {
const regex = /import\s+.*?\.(\w+);/;
const m = text.match(regex);
if (m) {
items.push({
name: m[1],
isDefault: false,
isNamespace: false,
nodeText: text
});
}
}
else if (match.check_id === 'java-import-static') {
const regex = /import\s+static\s+.*?\.(\w+)\.(\w+);/;
const m = text.match(regex);
if (m) {
items.push({
name: m[2],
isDefault: false,
isNamespace: false,
isStatic: true,
nodeText: text
});
}
}
}
else if (match.check_id.startsWith('cpp-')) {
const regex = /#include\s*[<"]([^>"]+)[>"]/;
const m = text.match(regex);
if (m) {
const headerName = m[1];
const baseName = path.basename(headerName, path.extname(headerName));
items.push({
name: baseName,
isDefault: false,
isNamespace: false,
nodeText: text
});
}
}
else if (match.check_id.startsWith('ruby-')) {
const regex = /(?:require|require_relative|load)\s*['"]([^'"]+)['"]/;
const m = text.match(regex);
if (m) {
const moduleName = m[1];
const baseName = path.basename(moduleName, path.extname(moduleName));
items.push({
name: baseName,
isDefault: false,
isNamespace: false,
nodeText: text
});
}
}
else if (match.check_id.startsWith('go-')) {
if (match.check_id === 'go-import-single') {
const regex = /import\s+["']([^"']+)["']/;
const m = text.match(regex);
if (m) {
const packagePath = m[1];
const packageName = path.basename(packagePath);
items.push({
name: packageName,
isDefault: false,
isNamespace: false,
nodeText: text
});
}
}
else if (match.check_id === 'go-import-alias') {
const regex = /import\s+(\w+)\s+["']([^"']+)["']/;
const m = text.match(regex);
if (m) {
items.push({
name: m[1],
isDefault: false,
isNamespace: false,
nodeText: text
});
}
}
}
else if (match.check_id.startsWith('php-')) {
if (match.check_id.includes('use')) {
const regex = /use\s+(.*?)(?:\s+as\s+(\w+))?;/;
const m = text.match(regex);
if (m) {
const namespace = m[1];
const alias = m[2];
const name = namespace.split('\\').pop() || '';
items.push({
name: alias || name,
alias: alias ? name : undefined,
isDefault: false,
isNamespace: false,
nodeText: text
});
}
}
else {
const regex = /(?:require|include)(?:_once)?\s*['"]([^'"]+)['"]/;
const m = text.match(regex);
if (m) {
const filePath = m[1];
const baseName = path.basename(filePath, path.extname(filePath));
items.push({
name: baseName,
isDefault: false,
isNamespace: false,
nodeText: text
});
}
}
}
}
catch (error) {
logger.warn({ err: error, match }, 'Error extracting imported items from Semgrep match');
}
return items;
}
isCorePath(importPath, checkId) {
if (checkId.startsWith('js-')) {
const nodeBuiltins = [
'assert', 'buffer', 'child_process', 'cluster', 'console', 'constants',
'crypto', 'dgram', 'dns', 'domain', 'events', 'fs', 'http', 'https',
'module', 'net', 'os', 'path', 'perf_hooks', 'process', 'punycode',
'querystring', 'readline', 'repl', 'stream', 'string_decoder', 'timers',
'tls', 'trace_events', 'tty', 'url', 'util', 'v8', 'vm', 'wasi', 'worker_threads',
'zlib'
];
return nodeBuiltins.includes(importPath);
}
else if (checkId.startsWith('python-')) {
const pythonBuiltins = [
'abc', 'argparse', 'asyncio', 'collections', 'copy', 'datetime',
'functools', 'glob', 'io', 'itertools', 'json', 'logging', 'math',
'os', 'pathlib', 're', 'shutil', 'sys', 'time', 'typing', 'uuid'
];
return pythonBuiltins.includes(importPath.split('.')[0]);
}
else if (checkId.startsWith('java-')) {
return importPath.startsWith('java.') ||
importPath.startsWith('javax.') ||
importPath.startsWith('sun.') ||
importPath.startsWith('com.sun.');
}
else if (checkId.startsWith('cpp-')) {
return checkId === 'cpp-include-system';
}
else if (checkId.startsWith('ruby-')) {
const rubyBuiltins = [
'abbrev', 'base64', 'benchmark', 'bigdecimal', 'cgi', 'csv',
'date', 'digest', 'fileutils', 'find', 'forwardable', 'io', 'json',
'logger', 'net', 'open-uri', 'optparse', 'pathname', 'pp', 'set',
'shellwords', 'stringio', 'strscan', 'tempfile', 'time', 'timeout',
'uri', 'yaml', 'zlib'
];
return rubyBuiltins.includes(importPath);
}
else if (checkId.startsWith('go-')) {
const goBuiltins = [
'bufio', 'bytes', 'context', 'crypto', 'database', 'encoding',
'errors', 'flag', 'fmt', 'io', 'log', 'math', 'net', 'os',
'path', 'reflect', 'regexp', 'runtime', 'sort', 'strconv',
'strings', 'sync', 'syscall', 'time', 'unicode'
];
return goBuiltins.some(pkg => importPath === pkg || importPath.startsWith(pkg + '/'));
}
else if (checkId.startsWith('php-')) {
return false;
}
return false;
}
getModuleSystem(checkId) {
if (checkId.startsWith('js-')) {
if (checkId === 'js-require') {
return 'commonjs';
}
else {
return 'esm';
}
}
else if (checkId.startsWith('python-')) {
return 'python';
}
else if (checkId.startsWith('java-')) {
return 'java';
}
else if (checkId.startsWith('cpp-')) {
return 'cpp';
}
else if (checkId.startsWith('ruby-')) {
return 'ruby';
}
else if (checkId.startsWith('go-')) {
return 'go';
}
else if (checkId.startsWith('php-')) {
return 'php';
}
return 'unknown';
}
dispose() {
this.cache.clear();
if (this.tempFiles && this.tempFiles.length > 0) {
this.tempFiles.forEach(file => {
try {
if (fs.existsSync(file)) {
fs.unlinkSync(file);
logger.debug({ file }, 'Deleted temporary file during SemgrepAdapter disposal');
}
}
catch (error) {
logger.warn({ file, error }, 'Failed to delete temporary file during SemgrepAdapter disposal');
}
});
this.tempFiles = [];
}
logger.debug('SemgrepAdapter disposed');
}
}