@sun-asterisk/sunlint
Version:
☀️ SunLint - Multi-language static analysis tool for code quality and security | Sun* Engineering Standards
331 lines (271 loc) • 10.1 kB
JavaScript
const fs = require('fs');
const path = require('path');
class S029Analyzer {
constructor() {
this.ruleId = 'S029';
this.ruleName = 'CSRF Protection Required';
this.description = 'Cần áp dụng cơ chế chống CSRF cho các chức năng xác thực';
}
async analyze(files, language, options = {}) {
const violations = [];
for (const filePath of files) {
if (options.verbose) {
console.log(`🔍 Running S029 analysis on ${path.basename(filePath)}`);
}
try {
const content = fs.readFileSync(filePath, 'utf8');
const fileViolations = await this.analyzeFile(filePath, content, language, options);
violations.push(...fileViolations);
} catch (error) {
console.warn(`⚠️ Failed to analyze ${filePath}: ${error.message}`);
}
}
return violations;
}
async analyzeFile(filePath, content, language, config) {
switch (language) {
case 'typescript':
case 'javascript':
return this.analyzeTypeScript(filePath, content, config);
default:
return [];
}
}
async analyzeTypeScript(filePath, content, config) {
const violations = [];
const lines = content.split('\n');
// Find lines where global CSRF protection is applied
const globalCSRFLines = this.findGlobalCSRFLines(lines);
lines.forEach((line, index) => {
const lineNumber = index + 1;
const trimmedLine = line.trim();
// Skip comments and imports
if (this.isCommentOrImport(trimmedLine)) {
return;
}
// Look for Express route handlers that need CSRF protection
const routeHandlers = this.findRouteHandlers(trimmedLine, line);
routeHandlers.forEach(handler => {
// Skip if this is a mock or test context
if (this.isMockOrTestContext(content, handler.instance)) {
return;
}
// Check if global CSRF protection was applied before this route
const hasGlobalCSRFProtection = this.hasGlobalCSRFProtectionBeforeLine(globalCSRFLines, index, handler.instance);
// Check if this specific route has CSRF protection
const hasRouteCSRFProtection = this.hasRouteSpecificCSRFProtection(lines, index, handler);
if (!hasGlobalCSRFProtection && !hasRouteCSRFProtection) {
violations.push({
ruleId: this.ruleId,
file: filePath,
line: lineNumber,
column: handler.column,
message: `CSRF protection is missing for route handler '${handler.route}'. Apply csurf() or equivalent middleware`,
severity: 'error',
code: trimmedLine,
type: 'missing_csrf_protection',
confidence: handler.confidence,
suggestion: 'Add CSRF middleware: app.use(csurf()) or use CSRF token validation'
});
}
});
});
return violations;
}
isCommentOrImport(line) {
const trimmed = line.trim();
return trimmed.startsWith('//') ||
trimmed.startsWith('/*') ||
trimmed.startsWith('*') ||
trimmed.startsWith('import ') ||
trimmed.startsWith('export ');
}
findRouteHandlers(line, originalLine) {
const handlers = [];
const foundMatches = new Set(); // Prevent duplicates
// Only detect Express.js route patterns, not HTTP client methods
const routePatterns = [
// Express method with middleware: app.post('/path', middleware, handler)
{
regex: /\b(app|router|server)\s*\.\s*(post|put|delete|patch)\s*\(\s*(['"`][^'"`]*['"`])\s*,/gi,
type: 'express_route_with_middleware',
priority: 1 // Higher priority to check first
},
// app.post(), router.put(), etc.
{
regex: /\b(app|router|server)\s*\.\s*(post|put|delete|patch)\s*\(\s*(['"`][^'"`]*['"`])/gi,
type: 'express_route',
priority: 2
}
];
// Sort by priority to avoid duplicates
routePatterns.sort((a, b) => a.priority - b.priority);
routePatterns.forEach(pattern => {
let match;
while ((match = pattern.regex.exec(line)) !== null) {
const instance = match[1]; // app, router, server
const method = match[2]; // post, put, delete, patch
const route = match[3]; // '/path'
const matchKey = `${instance}.${method}(${route})`; // Unique key
// Skip duplicates
if (foundMatches.has(matchKey)) {
continue;
}
// Skip if it's clearly not Express.js context
if (this.isNotExpressContext(line, instance)) {
continue;
}
foundMatches.add(matchKey);
handlers.push({
type: pattern.type,
instance: instance,
method: method,
route: route.replace(/['"]/g, ''),
column: match.index + 1,
confidence: this.calculateConfidence(line, pattern.type)
});
}
});
return handlers;
}
isNotExpressContext(line, instance) {
// Skip HTTP client methods (like axios, fetch wrappers)
const clientPatterns = [
'public ', 'private ', 'protected ', // Class methods
'async ', 'function ', // Function definitions
'const ', 'let ', 'var ', // Variable assignments
': Promise<', ': BaseResponse<', // TypeScript return types
'this.http', 'httpClient', // HTTP client instances
'axios.', 'fetch(', // HTTP client calls
];
const lowerLine = line.toLowerCase();
// If line contains client patterns, likely not Express route
const hasClientPattern = clientPatterns.some(pattern =>
lowerLine.includes(pattern.toLowerCase())
);
if (hasClientPattern) {
return true;
}
// If instance name suggests HTTP client, skip
const clientInstanceNames = ['httpclient', 'client', 'api', 'service'];
if (clientInstanceNames.includes(instance.toLowerCase())) {
return true;
}
return false;
}
// Check if the file content suggests this is a mock/test rather than real Express app
isMockOrTestContext(content, instance) {
const lowerContent = content.toLowerCase();
// Look for mock object definitions
const mockPatterns = [
`const ${instance.toLowerCase()} = {`,
`let ${instance.toLowerCase()} = {`,
`var ${instance.toLowerCase()} = {`,
`${instance.toLowerCase()}: {`,
];
const hasMockDefinition = mockPatterns.some(pattern =>
lowerContent.includes(pattern)
);
if (hasMockDefinition) {
return true;
}
// Check for test file patterns
const testIndicators = ['.test.', '.spec.', '__tests__', 'test case', 'mock'];
const isTestContext = testIndicators.some(indicator =>
lowerContent.includes(indicator)
);
return isTestContext;
}
hasCSRFProtection(content) {
const csrfPatterns = [
// Middleware usage
'csurf()',
'csrfProtection',
'verifyCsrfToken',
'checkCsrf',
'csrf-token',
'_csrf',
// Manual CSRF checks
'req.csrfToken',
'csrf.verify',
'validateCSRF',
// Security headers
'x-csrf-token',
'x-xsrf-token',
// Framework-specific
'protect_from_forgery', // Rails
'@csrf', // Laravel
];
const lowerContent = content.toLowerCase();
return csrfPatterns.some(pattern =>
lowerContent.includes(pattern.toLowerCase())
);
}
hasGlobalCSRFProtection(content) {
// Check for global CSRF middleware: app.use(csurf())
const globalPatterns = [
/\b(app|router|server)\s*\.\s*use\s*\(\s*csurf\(\)/gi,
/\b(app|router|server)\s*\.\s*use\s*\(\s*csrfProtection/gi,
/\b(app|router|server)\s*\.\s*use\s*\(\s*csrf\(\)/gi,
];
return globalPatterns.some(pattern => pattern.test(content));
}
findGlobalCSRFLines(lines) {
const csrfLines = [];
lines.forEach((line, index) => {
const globalPatterns = [
/\b(app|router|server)\s*\.\s*use\s*\(\s*csurf\(\)/gi,
/\b(app|router|server)\s*\.\s*use\s*\(\s*csrfProtection/gi,
/\b(app|router|server)\s*\.\s*use\s*\(\s*csrf\(\)/gi,
];
globalPatterns.forEach(pattern => {
let match;
while ((match = pattern.exec(line)) !== null) {
csrfLines.push({
lineIndex: index,
instance: match[1], // app, router, server
line: line.trim()
});
}
});
});
return csrfLines;
}
hasGlobalCSRFProtectionBeforeLine(globalCSRFLines, routeLineIndex, routeInstance) {
// Check if any global CSRF protection was applied for this instance before this route
return globalCSRFLines.some(csrf =>
csrf.instance === routeInstance && csrf.lineIndex < routeLineIndex
);
}
hasRouteSpecificCSRFProtection(lines, currentIndex, handler) {
// Check if the route has CSRF middleware as parameter
// e.g. app.post('/path', csrfProtection, handler)
const currentLine = lines[currentIndex];
const csrfMiddlewarePatterns = [
'csrfProtection',
'csurf()',
'verifyCsrfToken',
'checkCsrf',
];
return csrfMiddlewarePatterns.some(pattern =>
currentLine.includes(pattern)
);
}
calculateConfidence(line, patternType) {
let confidence = 0.8;
// Higher confidence for clear Express patterns
if (patternType === 'express_route_with_middleware') {
confidence += 0.1;
}
// Lower confidence if mixed with client-like patterns
const clientIndicators = ['public', 'class', 'Promise<', 'async'];
const hasClientIndicators = clientIndicators.some(indicator =>
line.includes(indicator)
);
if (hasClientIndicators) {
confidence -= 0.3;
}
return Math.max(0.3, Math.min(1.0, confidence));
}
}
module.exports = new S029Analyzer();