UNPKG

@sun-asterisk/sunlint

Version:

☀️ SunLint - Multi-language static analysis tool for code quality and security | Sun* Engineering Standards

251 lines (205 loc) 7.33 kB
const fs = require('fs'); const path = require('path'); class S027CategorizedAnalyzer { constructor() { this.ruleId = 'S027'; this.ruleName = 'No Hardcoded Secrets (Categorized)'; this.description = 'Phát hiện thông tin bảo mật theo categories với độ ưu tiên khác nhau'; // Load categories config this.config = this.loadConfig(); this.categories = this.config.categories; this.globalExcludePatterns = this.config.global_exclude_patterns.map(p => new RegExp(p, 'i')); this.minLength = this.config.min_length || 8; this.maxLength = this.config.max_length || 1000; // Compile patterns for performance this.compilePatterns(); } loadConfig() { const configPath = path.join(__dirname, 'categories.json'); try { const config = JSON.parse(fs.readFileSync(configPath, 'utf8')); return config.S027; } catch (error) { console.error('Failed to load S027 categories config:', error.message); return { categories: [], global_exclude_patterns: [] }; } } compilePatterns() { this.categories.forEach(category => { category.compiledPatterns = category.patterns.map(p => ({ regex: new RegExp(p, 'gm'), original: p })); if (category.exclude_patterns) { category.compiledExcludePatterns = category.exclude_patterns.map(p => new RegExp(p, 'i')); } }); } async analyze(files, language, options = {}) { const violations = []; this.currentFilePath = ''; for (const filePath of files) { // Skip build/dist/node_modules if (this.shouldSkipFile(filePath)) { continue; } this.currentFilePath = filePath; try { const content = fs.readFileSync(filePath, 'utf8'); const fileViolations = this.analyzeFile(content, filePath); violations.push(...fileViolations); } catch (error) { if (options.verbose) { console.error(`Error analyzing ${filePath}:`, error.message); } } } return violations; } shouldSkipFile(filePath) { const skipPatterns = [ 'build/', 'dist/', 'node_modules/', '.git/', 'coverage/', '.next/', '.cache/', 'tmp/', '.lock', '.log', '.min.js', '.bundle.js' ]; return skipPatterns.some(pattern => filePath.includes(pattern)); } analyzeFile(content, filePath) { const violations = []; // Handle different line endings (Windows \r\n, Unix \n, Mac \r) const lines = content.split(/\r?\n/); // Check if this is a test file for context const isTestFile = this.isTestFile(filePath); lines.forEach((line, index) => { const lineNumber = index + 1; const trimmedLine = line.trim(); // Skip comments and imports if (this.isCommentOrImport(trimmedLine)) { return; } // Check global exclude patterns first if (this.matchesGlobalExcludes(line)) { return; } // Check each category this.categories.forEach(category => { const categoryViolations = this.checkCategory( category, line, lineNumber, filePath, isTestFile ); violations.push(...categoryViolations); }); }); return violations; } isTestFile(filePath) { const testPatterns = [ /\.(test|spec)\./i, /__tests__/i, /\/tests?\//i, /\/spec\//i, /setupTests/i, /testSetup/i, /test[-_]/i, // Matches test- or test_ /^.*\/test[^\/]*\.js$/i // Matches files starting with test ]; return testPatterns.some(pattern => pattern.test(filePath)); } isCommentOrImport(line) { return line.startsWith('//') || line.startsWith('/*') || line.startsWith('import') || line.startsWith('export') || line.startsWith('*') || line.startsWith('<'); } matchesGlobalExcludes(line) { return this.globalExcludePatterns.some(pattern => pattern.test(line)); } checkCategory(category, line, lineNumber, filePath, isTestFile) { const violations = []; category.compiledPatterns.forEach(({ regex, original }) => { let match; // Reset regex lastIndex for global patterns regex.lastIndex = 0; while ((match = regex.exec(line)) !== null) { const matchedText = match[0]; const column = match.index + 1; // Check length constraints if (matchedText.length < this.minLength || matchedText.length > this.maxLength) { continue; } // Check category-specific excludes if (category.compiledExcludePatterns && category.compiledExcludePatterns.some(pattern => pattern.test(matchedText))) { continue; } // Be more lenient in test files for lower severity categories // But still report critical/high severity issues even in test files if (isTestFile && category.severity === 'low') { continue; } violations.push({ file: filePath, line: lineNumber, column: column, message: `[${category.name}] Potential ${category.severity} security risk: '${matchedText}'. ${category.description}`, severity: this.mapSeverity(category.severity), ruleId: this.ruleId, category: category.name, categoryDescription: category.description, matchedPattern: original, matchedText: matchedText }); } }); return violations; } mapSeverity(categorySeverity) { const severityMap = { 'critical': 'error', 'high': 'warning', 'medium': 'warning', 'low': 'info' }; return severityMap[categorySeverity] || 'warning'; } // Method for getting category statistics getCategoryStats(violations) { const stats = {}; violations.forEach(violation => { const category = violation.category; if (!stats[category]) { stats[category] = { count: 0, severity: violation.severity, files: new Set() }; } stats[category].count++; stats[category].files.add(violation.file); }); // Convert Set to array for JSON serialization Object.keys(stats).forEach(category => { stats[category].files = Array.from(stats[category].files); stats[category].fileCount = stats[category].files.length; }); return stats; } // Method for filtering by category filterByCategory(violations, categoryNames) { if (!categoryNames || categoryNames.length === 0) { return violations; } return violations.filter(violation => categoryNames.includes(violation.category) ); } // Method for filtering by severity filterBySeverity(violations, minSeverity = 'info') { const severityOrder = ['info', 'warning', 'error']; const minIndex = severityOrder.indexOf(minSeverity); if (minIndex === -1) return violations; return violations.filter(violation => { const violationIndex = severityOrder.indexOf(violation.severity); return violationIndex >= minIndex; }); } } module.exports = S027CategorizedAnalyzer;