UNPKG

code-auditor-mcp

Version:

Multi-language code quality auditor with MCP server - Analyze TypeScript, JavaScript, and Go code for SOLID principles, DRY violations, security patterns, and more

448 lines 15.2 kB
/** * Data Access Analyzer (Functional) * Analyzes database access patterns and data layer interactions * * Detects database usage, query patterns, performance risks, * and security concerns in data access code */ import { processFiles, createViolation, parseTypeScriptFile, getImports, findNodesByKind, getLineAndColumn, getNodeText } from './analyzerUtils.js'; import * as ts from 'typescript'; /** * Default configuration */ const DEFAULT_CONFIG = { databases: { 'primary': { name: 'Primary Database', importPatterns: ['/database/', '/db/', 'drizzle', 'prisma', 'typeorm'], queryPatterns: ['select', 'insert', 'update', 'delete', 'query'], ormPatterns: ['from', 'where', 'join', 'orderBy', 'groupBy'] }, 'secondary': { name: 'Secondary Database', importPatterns: ['/analytics/', '/reporting/'], queryPatterns: ['query', 'execute', 'run'], ormPatterns: [] } }, organizationPatterns: [ 'organizationId', 'organization_id', 'orgId', 'org_id', 'tenantId', 'tenant_id', 'companyId', 'company_id', 'filterByOrganization', 'whereOrganization', 'scopeToOrg' ], tablePatterns: { orm: [ /\.from\(['"`]?(\w+)['"`]?\)/g, /\.table\(['"`]?(\w+)['"`]?\)/g, /\.into\(['"`]?(\w+)['"`]?\)/g, /\.update\(['"`]?(\w+)['"`]?\)/g ], sql: [ /(?:FROM|JOIN|INTO|UPDATE)\s+['"`]?(\w+)['"`]?/gi, /(?:INSERT\s+INTO)\s+['"`]?(\w+)['"`]?/gi, /(?:DELETE\s+FROM)\s+['"`]?(\w+)['"`]?/gi ], queryBuilder: [ /table:\s*['"`](\w+)['"`]/g, /from:\s*['"`](\w+)['"`]/g ] }, performanceThresholds: { complexQueryCount: 3, unfilteredQueryCount: 5, joinedTableCount: 2 }, securityPatterns: { sqlInjectionRisks: [ 'concatenation', '${', 'string interpolation', '+ variable', 'raw(', 'unsafeRaw' ], parameterizedQueries: [ 'prepared', 'parameterized', 'bind', '?', '$1', ':param' ] }, sourcePatterns: { api: ['/api/', '/routes/', '/endpoints/'], page: ['/pages/', '/app/', '/views/'], service: ['/services/', '/lib/', '/utils/'] } }; /** * Analyze a single file for data access patterns */ async function analyzeFile(filePath, config) { const { sourceFile, errors } = await parseTypeScriptFile(filePath); if (errors.length > 0) { throw new Error(`Parse errors: ${errors.map(e => e.messageText).join(', ')}`); } const imports = getImports(sourceFile); const patterns = []; const violations = []; // Extract database calls const dbCalls = extractDatabaseCalls(sourceFile, imports, filePath, config); // Group calls by database type const callsByDb = new Map(); dbCalls.forEach(call => { if (!callsByDb.has(call.type)) { callsByDb.set(call.type, []); } callsByDb.get(call.type).push(call); }); // Create patterns for each database type used callsByDb.forEach((calls, dbType) => { const allTables = new Set(); const queries = []; let hasOrgFilter = false; let hasSqlInjectionRisk = false; calls.forEach(call => { call.tables.forEach(table => allTables.add(table)); hasOrgFilter = hasOrgFilter || call.hasOrganizationFilter; hasSqlInjectionRisk = hasSqlInjectionRisk || call.hasSqlInjectionRisk; // Create query info with enhanced analysis const queryInfo = extractQueryInfo(call, config); queries.push(queryInfo); }); const pattern = { source: detectSourceType(filePath, config), filePath, database: dbType, tables: Array.from(allTables), queries, hasOrganizationFilter: hasOrgFilter, performanceRisk: assessPerformanceRisk(queries, config), hasSqlInjectionRisk }; patterns.push(pattern); // Check for violations violations.push(...checkDataAccessViolations(pattern)); }); return { patterns, violations }; } /** * Detect source type based on file path */ function detectSourceType(filePath, config) { if (config.sourcePatterns.api.some(pattern => filePath.includes(pattern))) { return 'api'; } else if (config.sourcePatterns.page.some(pattern => filePath.includes(pattern))) { return 'component'; } else if (config.sourcePatterns.service.some(pattern => filePath.includes(pattern))) { return 'service'; } return 'service'; } /** * Extract database calls from the AST */ function extractDatabaseCalls(sourceFile, imports, filePath, config) { const calls = []; // Map imports to database types const dbImports = mapDatabaseImports(imports, config); // Find all call expressions const callExpressions = findNodesByKind(sourceFile, ts.SyntaxKind.CallExpression); callExpressions.forEach(callExpr => { const callText = getNodeText(callExpr, sourceFile); const { line, column } = getLineAndColumn(sourceFile, callExpr.getStart()); // Check each configured database Object.entries(dbImports).forEach(([dbType, importInfo]) => { if (importInfo.hasImports && isDatabaseCall(callText, importInfo)) { const tables = extractTablesFromCall(callText, config); const hasOrgFilter = checkOrganizationFilter(callText, config); const securityCheck = checkQuerySecurity(callText, config); calls.push({ type: dbType, method: extractMethodName(callExpr), file: filePath, line, column, tables, hasOrganizationFilter: hasOrgFilter, hasParameterizedQuery: securityCheck.parameterized, hasSqlInjectionRisk: securityCheck.injectionRisk }); } }); }); return calls; } /** * Map imports to database types */ function mapDatabaseImports(imports, config) { const result = {}; Object.entries(config.databases).forEach(([key, dbConfig]) => { const importNames = []; let hasImports = false; imports.forEach(imp => { if (dbConfig.importPatterns.some((pattern) => imp.moduleSpecifier.includes(pattern))) { hasImports = true; importNames.push(...imp.importedNames); } }); result[key] = { hasImports, importNames, patterns: [...dbConfig.queryPatterns, ...importNames] }; }); return result; } /** * Check if a call is a database call */ function isDatabaseCall(callText, importInfo) { if (!importInfo.hasImports) return false; return importInfo.patterns.some(pattern => callText.includes(pattern)); } /** * Extract method name from call expression */ function extractMethodName(callExpr) { const expression = callExpr.expression; if (ts.isPropertyAccessExpression(expression)) { return expression.name.text; } else if (ts.isIdentifier(expression)) { return expression.text; } return 'unknown'; } /** * Extract table names from call */ function extractTablesFromCall(callText, config) { const tables = new Set(); // Try all table extraction patterns Object.values(config.tablePatterns).forEach(patterns => { patterns.forEach(pattern => { const matches = Array.from(callText.matchAll(pattern)); matches.forEach(match => { if (match[1]) { tables.add(match[1].toLowerCase()); } }); }); }); return Array.from(tables); } /** * Check for organization filtering */ function checkOrganizationFilter(callText, config) { return config.organizationPatterns.some(pattern => callText.toLowerCase().includes(pattern.toLowerCase())); } /** * Check query security */ function checkQuerySecurity(callText, config) { const hasParameterized = config.securityPatterns.parameterizedQueries.some(pattern => callText.includes(pattern)); const hasInjectionRisk = config.securityPatterns.sqlInjectionRisks.some(pattern => callText.includes(pattern)); return { parameterized: hasParameterized, injectionRisk: hasInjectionRisk && !hasParameterized }; } /** * Extract query info from database call */ function extractQueryInfo(call, config) { return { type: inferQueryType(call.method), tables: call.tables, line: call.line, hasJoins: detectJoins(call.method), hasOrganizationFilter: call.hasOrganizationFilter, complexity: analyzeQueryComplexity(call, config) }; } /** * Infer query type from method name */ function inferQueryType(method) { const methodLower = method.toLowerCase(); if (methodLower.includes('select') || methodLower.includes('find') || methodLower.includes('get')) { return 'select'; } else if (methodLower.includes('insert') || methodLower.includes('create')) { return 'insert'; } else if (methodLower.includes('update')) { return 'update'; } else if (methodLower.includes('delete') || methodLower.includes('remove')) { return 'delete'; } return 'other'; } /** * Detect if a query has joins */ function detectJoins(method) { const joinPatterns = [ 'join', 'leftJoin', 'rightJoin', 'innerJoin', 'outerJoin', 'fullJoin' ]; return joinPatterns.some(pattern => method.toLowerCase().includes(pattern.toLowerCase())); } /** * Analyze query complexity */ function analyzeQueryComplexity(call, config) { let complexityScore = 0; // Table count factor if (call.tables.length > config.performanceThresholds.joinedTableCount) { complexityScore += 2; } else if (call.tables.length > 1) { complexityScore += 1; } // Method complexity if (detectJoins(call.method)) { complexityScore += 2; } // Security risk adds complexity if (call.hasSqlInjectionRisk) { complexityScore += 3; } // Missing org filter in multi-tenant context if (!call.hasOrganizationFilter && call.tables.length > 0) { complexityScore += 1; } // Determine complexity level if (complexityScore >= 5) return 'complex'; if (complexityScore >= 2) return 'moderate'; return 'simple'; } /** * Assess performance risk based on queries */ function assessPerformanceRisk(queries, config) { // Count complex queries const complexQueries = queries.filter(q => q.hasJoins || q.tables.length > config.performanceThresholds.joinedTableCount || q.complexity === 'complex').length; // Count queries without org filter const unfiltered = queries.filter(q => !q.hasOrganizationFilter).length; if (complexQueries > config.performanceThresholds.complexQueryCount || unfiltered > config.performanceThresholds.unfilteredQueryCount) { return 'high'; } else if (complexQueries > 1 || unfiltered > 2) { return 'medium'; } return 'low'; } /** * Check for data access violations */ function checkDataAccessViolations(pattern) { const violations = []; // Check for missing organization filter in multi-tenant scenarios if (!pattern.hasOrganizationFilter && pattern.tables.length > 0) { violations.push(createViolation({ analyzer: 'data-access', severity: 'warning', type: 'data-access', file: pattern.filePath, line: 1, column: 1, message: 'Database queries may be missing tenant/organization filtering', recommendation: 'Add appropriate filtering to ensure data isolation in multi-tenant environments', estimatedEffort: 'small' })); } // Check for SQL injection risks if (pattern.hasSqlInjectionRisk) { violations.push(createViolation({ analyzer: 'data-access', severity: 'critical', type: 'data-access', file: pattern.filePath, line: 1, column: 1, message: 'Potential SQL injection vulnerability detected', recommendation: 'Use parameterized queries or prepared statements instead of string concatenation', estimatedEffort: 'medium' })); } // Check for performance risks if (pattern.performanceRisk === 'high') { violations.push(createViolation({ analyzer: 'data-access', severity: 'warning', type: 'data-access', file: pattern.filePath, line: 1, column: 1, message: 'High performance risk detected in data access patterns', recommendation: 'Optimize queries, add indexes, implement caching, or paginate results', estimatedEffort: 'medium' })); } // Check for direct database access in presentation layer if (pattern.source === 'component') { violations.push(createViolation({ analyzer: 'data-access', severity: 'suggestion', type: 'data-access', file: pattern.filePath, line: 1, column: 1, message: 'Direct database access detected in presentation layer', recommendation: 'Consider moving data access to service layer or API endpoints for better separation of concerns', estimatedEffort: 'medium' })); } return violations; } /** * Data Access Analyzer definition */ export const dataAccessAnalyzer = { name: 'data-access', defaultConfig: DEFAULT_CONFIG, analyze: async (files, config, options, progressCallback) => { const mergedConfig = { ...DEFAULT_CONFIG, ...config }; // Custom processor to handle patterns collection const patterns = []; const allViolations = []; const result = await processFiles(files, async (filePath, sourceFile) => { const fileResult = await analyzeFile(filePath, mergedConfig); patterns.push(...fileResult.patterns); allViolations.push(...fileResult.violations); return fileResult.violations; }, 'data-access', mergedConfig, progressCallback ? (current, total, file) => progressCallback({ current, total, analyzer: 'data-access', file }) : undefined); // Override violations with our collected ones result.violations = allViolations; return result; } }; //# sourceMappingURL=dataAccessAnalyzer.js.map