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
JavaScript
/**
* 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