@sun-asterisk/sunlint 1.2.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -1
- package/CONTRIBUTING.md +533 -70
- package/README.md +16 -2
- package/config/engines/engines-enhanced.json +86 -0
- package/config/engines/semantic-config.json +114 -0
- package/config/eslint-rule-mapping.json +50 -38
- package/config/rule-analysis-strategies.js +18 -2
- package/config/rules/enhanced-rules-registry.json +2503 -0
- package/config/rules/rules-registry-generated.json +785 -837
- package/core/adapters/sunlint-rule-adapter.js +25 -30
- package/core/analysis-orchestrator.js +42 -2
- package/core/categories.js +52 -0
- package/core/category-constants.js +39 -0
- package/core/cli-action-handler.js +32 -5
- package/core/config-manager.js +111 -0
- package/core/config-merger.js +61 -0
- package/core/constants/categories.js +168 -0
- package/core/constants/defaults.js +165 -0
- package/core/constants/engines.js +185 -0
- package/core/constants/index.js +30 -0
- package/core/constants/rules.js +215 -0
- package/core/file-targeting-service.js +128 -7
- package/core/interfaces/rule-plugin.interface.js +207 -0
- package/core/plugin-manager.js +448 -0
- package/core/rule-selection-service.js +42 -15
- package/core/semantic-engine.js +560 -0
- package/core/semantic-rule-base.js +433 -0
- package/core/unified-rule-registry.js +484 -0
- package/docs/CONSTANTS-ARCHITECTURE.md +288 -0
- package/engines/core/base-engine.js +249 -0
- package/engines/engine-factory.js +275 -0
- package/engines/eslint-engine.js +180 -30
- package/engines/heuristic-engine.js +513 -56
- package/integrations/eslint/plugin/index.js +27 -27
- package/package.json +11 -6
- package/rules/README.md +252 -0
- package/rules/common/C002_no_duplicate_code/analyzer.js +65 -0
- package/rules/common/C002_no_duplicate_code/config.json +23 -0
- package/rules/common/C003_no_vague_abbreviations/analyzer.js +418 -0
- package/rules/common/C003_no_vague_abbreviations/config.json +35 -0
- package/rules/common/C006_function_naming/analyzer.js +504 -0
- package/rules/common/C006_function_naming/config.json +86 -0
- package/rules/common/C006_function_naming/smart-analyzer.js +503 -0
- package/rules/common/C010_limit_block_nesting/analyzer.js +389 -0
- package/rules/common/C012_command_query_separation/analyzer.js +481 -0
- package/rules/common/C012_command_query_separation/ast-analyzer.js +495 -0
- package/rules/common/C013_no_dead_code/analyzer.js +206 -0
- package/rules/common/C014_dependency_injection/analyzer.js +338 -0
- package/rules/common/C017_constructor_logic/analyzer.js +314 -0
- package/rules/common/C019_log_level_usage/analyzer.js +362 -0
- package/rules/common/C019_log_level_usage/config.json +121 -0
- package/rules/common/C029_catch_block_logging/analyzer-smart-pipeline.js +755 -0
- package/rules/common/C029_catch_block_logging/analyzer.js +141 -0
- package/rules/common/C029_catch_block_logging/config.json +59 -0
- package/rules/common/C031_validation_separation/analyzer.js +186 -0
- package/rules/common/C041_no_sensitive_hardcode/analyzer.js +292 -0
- package/rules/common/C041_no_sensitive_hardcode/ast-analyzer.js +296 -0
- package/rules/common/C042_boolean_name_prefix/analyzer.js +300 -0
- package/rules/common/C043_no_console_or_print/analyzer.js +431 -0
- package/rules/common/C047_no_duplicate_retry_logic/analyzer.js +590 -0
- package/rules/common/C047_no_duplicate_retry_logic/c047-semantic-rule.js +278 -0
- package/rules/common/C047_no_duplicate_retry_logic/symbol-analyzer-enhanced.js +968 -0
- package/rules/common/C047_no_duplicate_retry_logic/symbol-config.json +71 -0
- package/rules/common/C075_explicit_return_types/analyzer.js +103 -0
- package/rules/common/C076_single_test_behavior/analyzer.js +121 -0
- package/rules/docs/C002_no_duplicate_code.md +57 -0
- package/rules/docs/C031_validation_separation.md +72 -0
- package/rules/index.js +162 -0
- package/rules/migration/converter.js +385 -0
- package/rules/migration/mapping.json +164 -0
- package/rules/parser/constants.js +31 -0
- package/rules/parser/file-config.js +80 -0
- package/rules/parser/rule-parser-simple.js +305 -0
- package/rules/parser/rule-parser.js +527 -0
- package/rules/security/S015_insecure_tls_certificate/analyzer.js +150 -0
- package/rules/security/S015_insecure_tls_certificate/ast-analyzer.js +237 -0
- package/rules/security/S023_no_json_injection/analyzer.js +278 -0
- package/rules/security/S023_no_json_injection/ast-analyzer.js +359 -0
- package/rules/security/S026_json_schema_validation/analyzer.js +251 -0
- package/rules/security/S026_json_schema_validation/config.json +27 -0
- package/rules/security/S027_no_hardcoded_secrets/analyzer.js +436 -0
- package/rules/security/S027_no_hardcoded_secrets/config.json +29 -0
- package/rules/security/S029_csrf_protection/analyzer.js +330 -0
- package/rules/tests/C002_no_duplicate_code.test.js +50 -0
- package/rules/utils/ast-utils.js +191 -0
- package/rules/utils/base-analyzer.js +98 -0
- package/rules/utils/pattern-matchers.js +239 -0
- package/rules/utils/rule-helpers.js +264 -0
- package/rules/utils/severity-constants.js +93 -0
- package/scripts/category-manager.js +150 -0
- package/scripts/generate-rules-registry.js +88 -0
- package/scripts/generate_insights.js +188 -0
- package/scripts/migrate-rule-registry.js +157 -0
- package/scripts/validate-system.js +48 -0
- package/.sunlint.json +0 -35
- package/config/README.md +0 -88
- package/config/engines/eslint-rule-mapping.json +0 -74
- package/config/testing/test-s005-working.ts +0 -22
- package/engines/tree-sitter-parser.js +0 -0
- package/engines/universal-ast-engine.js +0 -0
- package/scripts/merge-reports.js +0 -424
- package/scripts/test-scripts/README.md +0 -22
- package/scripts/test-scripts/test-c041-comparison.js +0 -114
- package/scripts/test-scripts/test-c041-eslint.js +0 -67
- package/scripts/test-scripts/test-eslint-rules.js +0 -146
- package/scripts/test-scripts/test-real-world.js +0 -44
- package/scripts/test-scripts/test-rules-on-real-projects.js +0 -86
- /package/{config/schemas/sunlint-schema.json → rules/universal/C010/generic.js} +0 -0
- /package/{core/multi-rule-runner.js → rules/universal/C010/tree-sitter-analyzer.js} +0 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* C029 Analyzer - Smart Pipeline Integration
|
|
3
|
+
*
|
|
4
|
+
* This analyzer forwards to the Smart Pipeline for superior accuracy and performance
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
class C029Analyzer {
|
|
11
|
+
constructor(options = {}) {
|
|
12
|
+
this.ruleId = 'C029';
|
|
13
|
+
this.ruleName = 'Enhanced Catch Block Error Logging';
|
|
14
|
+
this.description = 'Mọi catch block phải log nguyên nhân lỗi đầy đủ và bảo toàn context (Smart Pipeline 3-stage analysis)';
|
|
15
|
+
this.verbose = options.verbose || false;
|
|
16
|
+
|
|
17
|
+
// Load Smart Pipeline as primary analyzer
|
|
18
|
+
this.smartPipeline = null;
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
this.smartPipeline = require('./analyzer-smart-pipeline.js');
|
|
22
|
+
if (this.verbose) {
|
|
23
|
+
console.log('[DEBUG] 🎯 C029: Smart Pipeline loaded (3-stage: Regex → AST → Data Flow)');
|
|
24
|
+
}
|
|
25
|
+
} catch (error) {
|
|
26
|
+
if (this.verbose) {
|
|
27
|
+
console.warn('[DEBUG] ⚠️ C029: Smart Pipeline failed, using fallback:', error.message);
|
|
28
|
+
}
|
|
29
|
+
this.smartPipeline = null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async analyze(files, language, options = {}) {
|
|
34
|
+
// Store verbose option for this analysis
|
|
35
|
+
this.verbose = options.verbose || this.verbose || false;
|
|
36
|
+
|
|
37
|
+
// Use Smart Pipeline as primary choice
|
|
38
|
+
if (this.smartPipeline) {
|
|
39
|
+
if (this.verbose) {
|
|
40
|
+
console.log('[DEBUG] 🎯 C029: Using Smart Pipeline (3-stage analysis)...');
|
|
41
|
+
}
|
|
42
|
+
return await this.smartPipeline.analyze(files, language, options);
|
|
43
|
+
} else {
|
|
44
|
+
if (this.verbose) {
|
|
45
|
+
console.log('[DEBUG] 🔍 C029: Using fallback regex analysis...');
|
|
46
|
+
}
|
|
47
|
+
return await this.analyzeWithRegex(files, language, options);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async analyzeWithRegex(files, language, options = {}) {
|
|
52
|
+
const violations = [];
|
|
53
|
+
|
|
54
|
+
for (const filePath of files) {
|
|
55
|
+
if (options.verbose) {
|
|
56
|
+
console.log(`🔍 C029 Regex: Processing ${path.basename(filePath)}...`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
61
|
+
const fileViolations = await this.analyzeFile(filePath, content, language);
|
|
62
|
+
violations.push(...fileViolations);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.warn(`⚠️ C029: Error processing ${filePath}:`, error.message);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return violations;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async analyzeFile(filePath, content, language) {
|
|
72
|
+
const violations = [];
|
|
73
|
+
const lines = content.split('\n');
|
|
74
|
+
|
|
75
|
+
for (let i = 0; i < lines.length; i++) {
|
|
76
|
+
const line = lines[i];
|
|
77
|
+
|
|
78
|
+
// Simple catch block detection
|
|
79
|
+
if (line.includes('catch') && line.includes('(')) {
|
|
80
|
+
const catchBlock = this.extractCatchBlock(lines, i);
|
|
81
|
+
|
|
82
|
+
if (this.isCatchBlockEmpty(catchBlock.content)) {
|
|
83
|
+
violations.push({
|
|
84
|
+
file: filePath,
|
|
85
|
+
line: i + 1,
|
|
86
|
+
column: line.indexOf('catch') + 1,
|
|
87
|
+
message: 'Empty catch block detected',
|
|
88
|
+
severity: 'error',
|
|
89
|
+
ruleId: this.ruleId,
|
|
90
|
+
type: 'empty_catch'
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return violations;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
extractCatchBlock(lines, startIndex) {
|
|
100
|
+
const content = [];
|
|
101
|
+
let braceCount = 0;
|
|
102
|
+
let inBlock = false;
|
|
103
|
+
|
|
104
|
+
for (let i = startIndex; i < lines.length; i++) {
|
|
105
|
+
const line = lines[i];
|
|
106
|
+
content.push(line);
|
|
107
|
+
|
|
108
|
+
for (const char of line) {
|
|
109
|
+
if (char === '{') {
|
|
110
|
+
braceCount++;
|
|
111
|
+
inBlock = true;
|
|
112
|
+
} else if (char === '}') {
|
|
113
|
+
braceCount--;
|
|
114
|
+
if (braceCount === 0 && inBlock) {
|
|
115
|
+
return { content, endIndex: i };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return { content, endIndex: startIndex };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
isCatchBlockEmpty(content) {
|
|
125
|
+
const blockContent = content.join('\n');
|
|
126
|
+
|
|
127
|
+
// Remove comments and whitespace
|
|
128
|
+
const cleanContent = blockContent
|
|
129
|
+
.replace(/\/\*[\s\S]*?\*\//g, '') // Remove multi-line comments
|
|
130
|
+
.replace(/\/\/.*$/gm, '') // Remove single-line comments
|
|
131
|
+
.replace(/\s+/g, ' ') // Normalize whitespace
|
|
132
|
+
.trim();
|
|
133
|
+
|
|
134
|
+
// Check if only contains catch declaration and braces
|
|
135
|
+
const hasOnlyStructure = /^catch\s*\([^)]*\)\s*\{\s*\}$/.test(cleanContent);
|
|
136
|
+
|
|
137
|
+
return hasOnlyStructure;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
module.exports = C029Analyzer;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"ruleId": "C029",
|
|
3
|
+
"name": "Catch Block Error Logging",
|
|
4
|
+
"description": "Mọi catch block phải log nguyên nhân lỗi đầy đủ",
|
|
5
|
+
"category": "error-handling",
|
|
6
|
+
"severity": "error",
|
|
7
|
+
"languages": ["typescript", "dart", "kotlin", "javascript"],
|
|
8
|
+
"version": "1.0.0",
|
|
9
|
+
"status": "activated",
|
|
10
|
+
"tags": ["error-handling", "logging", "debugging", "monitoring"],
|
|
11
|
+
"examples": {
|
|
12
|
+
"typescript": {
|
|
13
|
+
"violations": [
|
|
14
|
+
"try { riskyOperation(); } catch (error) { return null; }",
|
|
15
|
+
"catch (e) { /* empty */ }",
|
|
16
|
+
"catch (error) { throw new Error('Failed'); }"
|
|
17
|
+
],
|
|
18
|
+
"valid": [
|
|
19
|
+
"catch (error) { console.error('Operation failed:', error); }",
|
|
20
|
+
"catch (e) { logger.error('Error in process:', e); }",
|
|
21
|
+
"catch (error) { console.error(error); throw error; }"
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
"dart": {
|
|
25
|
+
"violations": [
|
|
26
|
+
"try { riskyOperation(); } catch (e) { return null; }",
|
|
27
|
+
"on Exception catch (e) { /* empty */ }"
|
|
28
|
+
],
|
|
29
|
+
"valid": [
|
|
30
|
+
"catch (e) { print('Error: $e'); }",
|
|
31
|
+
"on Exception catch (e) { log.severe('Failed:', e); }"
|
|
32
|
+
]
|
|
33
|
+
},
|
|
34
|
+
"kotlin": {
|
|
35
|
+
"violations": [
|
|
36
|
+
"try { riskyOperation() } catch (e: Exception) { return null }",
|
|
37
|
+
"catch (e: Exception) { /* empty */ }"
|
|
38
|
+
],
|
|
39
|
+
"valid": [
|
|
40
|
+
"catch (e: Exception) { Log.e(TAG, 'Error:', e) }",
|
|
41
|
+
"catch (e: Exception) { logger.error('Failed', e) }"
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"configuration": {
|
|
46
|
+
"requiredLoggingMethods": [
|
|
47
|
+
"console.error",
|
|
48
|
+
"console.log",
|
|
49
|
+
"logger.error",
|
|
50
|
+
"log.error",
|
|
51
|
+
"print",
|
|
52
|
+
"log.severe",
|
|
53
|
+
"Log.e",
|
|
54
|
+
"timber.e"
|
|
55
|
+
],
|
|
56
|
+
"allowEmptyWhenRethrow": true,
|
|
57
|
+
"checkErrorParameter": true
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Rule C031 - Validation Logic Separation
|
|
6
|
+
* Kiểm tra logic validation có bị trộn lẫn với business logic không
|
|
7
|
+
*/
|
|
8
|
+
class ValidationSeparationAnalyzer {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.ruleId = 'C031';
|
|
11
|
+
this.ruleName = 'Validation Logic Separation';
|
|
12
|
+
this.category = 'architecture';
|
|
13
|
+
this.severity = 'warning';
|
|
14
|
+
this.description = 'Logic kiểm tra dữ liệu (validate) phải nằm riêng biệt';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
analyzeFile(filePath, options = {}) {
|
|
18
|
+
const violations = [];
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
if (!fs.existsSync(filePath)) {
|
|
22
|
+
return violations;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
26
|
+
const lines = content.split('\n');
|
|
27
|
+
|
|
28
|
+
// Detect functions with mixed validation and business logic
|
|
29
|
+
const functions = this.extractFunctions(content);
|
|
30
|
+
|
|
31
|
+
for (const func of functions) {
|
|
32
|
+
const validationCount = this.countValidationStatements(func.body);
|
|
33
|
+
const businessLogicCount = this.countBusinessLogicStatements(func.body);
|
|
34
|
+
|
|
35
|
+
// If both validation and business logic exist in same function
|
|
36
|
+
if (validationCount > 0 && businessLogicCount > 0) {
|
|
37
|
+
const maxValidationAllowed = options.maxValidationStatementsInFunction || 3;
|
|
38
|
+
|
|
39
|
+
if (validationCount > maxValidationAllowed) {
|
|
40
|
+
violations.push({
|
|
41
|
+
line: func.startLine,
|
|
42
|
+
column: 1,
|
|
43
|
+
message: `Function '${func.name}' has ${validationCount} validation statements mixed with business logic. Consider separating validation logic.`,
|
|
44
|
+
ruleId: this.ruleId,
|
|
45
|
+
severity: this.severity,
|
|
46
|
+
source: lines[func.startLine - 1]?.trim() || ''
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
} catch (error) {
|
|
53
|
+
console.error(`Error analyzing ${filePath}:`, error.message);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return violations;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
extractFunctions(content) {
|
|
60
|
+
const functions = [];
|
|
61
|
+
const lines = content.split('\n');
|
|
62
|
+
|
|
63
|
+
// Simple function detection patterns
|
|
64
|
+
const functionPatterns = [
|
|
65
|
+
/function\s+(\w+)\s*\(/g,
|
|
66
|
+
/const\s+(\w+)\s*=\s*\(/g,
|
|
67
|
+
/(\w+)\s*\(\s*[^)]*\s*\)\s*=>/g,
|
|
68
|
+
/(\w+)\s*:\s*function\s*\(/g
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
for (let i = 0; i < lines.length; i++) {
|
|
72
|
+
const line = lines[i];
|
|
73
|
+
|
|
74
|
+
for (const pattern of functionPatterns) {
|
|
75
|
+
const matches = line.matchAll(pattern);
|
|
76
|
+
for (const match of matches) {
|
|
77
|
+
const functionName = match[1];
|
|
78
|
+
const startLine = i + 1;
|
|
79
|
+
|
|
80
|
+
// Extract function body (simple approach)
|
|
81
|
+
const body = this.extractFunctionBody(lines, i);
|
|
82
|
+
|
|
83
|
+
functions.push({
|
|
84
|
+
name: functionName,
|
|
85
|
+
startLine,
|
|
86
|
+
body
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return functions;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
extractFunctionBody(lines, startIndex) {
|
|
96
|
+
let body = '';
|
|
97
|
+
let braceCount = 0;
|
|
98
|
+
let inFunction = false;
|
|
99
|
+
|
|
100
|
+
for (let i = startIndex; i < lines.length; i++) {
|
|
101
|
+
const line = lines[i];
|
|
102
|
+
|
|
103
|
+
if (line.includes('{')) {
|
|
104
|
+
braceCount += (line.match(/\{/g) || []).length;
|
|
105
|
+
inFunction = true;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (inFunction) {
|
|
109
|
+
body += line + '\n';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (line.includes('}')) {
|
|
113
|
+
braceCount -= (line.match(/\}/g) || []).length;
|
|
114
|
+
if (braceCount <= 0 && inFunction) {
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return body;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
countValidationStatements(code) {
|
|
124
|
+
const validationPatterns = [
|
|
125
|
+
/if\s*\(\s*!.*\)\s*\{?\s*throw/g,
|
|
126
|
+
/if\s*\(.*\.\s*length\s*[<>=]\s*\d+\)/g,
|
|
127
|
+
/if\s*\(.*\s*==\s*null\s*\||\s*.*\s*==\s*undefined\)/g,
|
|
128
|
+
/if\s*\(.*\s*!\s*=\s*null\s*&&\s*.*\s*!\s*=\s*undefined\)/g,
|
|
129
|
+
/throw\s+new\s+Error\s*\(/g,
|
|
130
|
+
/assert\s*\(/g,
|
|
131
|
+
/validate\w*\s*\(/g,
|
|
132
|
+
/check\w*\s*\(/g
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
let count = 0;
|
|
136
|
+
for (const pattern of validationPatterns) {
|
|
137
|
+
const matches = code.match(pattern);
|
|
138
|
+
if (matches) {
|
|
139
|
+
count += matches.length;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return count;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
countBusinessLogicStatements(code) {
|
|
147
|
+
const businessLogicPatterns = [
|
|
148
|
+
/calculate\w*\s*\(/g,
|
|
149
|
+
/process\w*\s*\(/g,
|
|
150
|
+
/save\w*\s*\(/g,
|
|
151
|
+
/update\w*\s*\(/g,
|
|
152
|
+
/delete\w*\s*\(/g,
|
|
153
|
+
/send\w*\s*\(/g,
|
|
154
|
+
/return\s+\w+\s*\(/g,
|
|
155
|
+
/await\s+\w+\s*\(/g
|
|
156
|
+
];
|
|
157
|
+
|
|
158
|
+
let count = 0;
|
|
159
|
+
for (const pattern of businessLogicPatterns) {
|
|
160
|
+
const matches = code.match(pattern);
|
|
161
|
+
if (matches) {
|
|
162
|
+
count += matches.length;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return count;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Main analyze method expected by CLI
|
|
170
|
+
async analyze(files, language, config) {
|
|
171
|
+
const violations = [];
|
|
172
|
+
|
|
173
|
+
for (const filePath of files) {
|
|
174
|
+
try {
|
|
175
|
+
const fileViolations = this.analyzeFile(filePath, config);
|
|
176
|
+
violations.push(...fileViolations);
|
|
177
|
+
} catch (error) {
|
|
178
|
+
console.error(`Error analyzing file ${filePath}:`, error.message);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return violations;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
module.exports = new ValidationSeparationAnalyzer();
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
class C041Analyzer {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.ruleId = 'C041';
|
|
7
|
+
this.ruleName = 'No Hardcoded Sensitive Information';
|
|
8
|
+
this.description = 'Không hardcode hoặc push thông tin nhạy cảm vào repo';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async analyze(files, language, options = {}) {
|
|
12
|
+
const violations = [];
|
|
13
|
+
|
|
14
|
+
for (const filePath of files) {
|
|
15
|
+
if (options.verbose) {
|
|
16
|
+
console.log(`🔍 Running C041 analysis on ${path.basename(filePath)}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
21
|
+
const fileViolations = await this.analyzeFile(filePath, content, language, options);
|
|
22
|
+
violations.push(...fileViolations);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
console.warn(`⚠️ Failed to analyze ${filePath}: ${error.message}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return violations;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async analyzeFile(filePath, content, language, config) {
|
|
32
|
+
switch (language) {
|
|
33
|
+
case 'typescript':
|
|
34
|
+
case 'javascript':
|
|
35
|
+
return this.analyzeTypeScript(filePath, content, config);
|
|
36
|
+
default:
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async analyzeTypeScript(filePath, content, config) {
|
|
42
|
+
const violations = [];
|
|
43
|
+
const lines = content.split('\n');
|
|
44
|
+
|
|
45
|
+
lines.forEach((line, index) => {
|
|
46
|
+
const lineNumber = index + 1;
|
|
47
|
+
const trimmedLine = line.trim();
|
|
48
|
+
|
|
49
|
+
// Skip comments and imports
|
|
50
|
+
if (this.isCommentOrImport(trimmedLine)) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Find potential hardcoded sensitive values
|
|
55
|
+
const sensitiveMatches = this.findSensitiveHardcode(trimmedLine, line);
|
|
56
|
+
|
|
57
|
+
sensitiveMatches.forEach(match => {
|
|
58
|
+
violations.push({
|
|
59
|
+
ruleId: this.ruleId,
|
|
60
|
+
file: filePath,
|
|
61
|
+
line: lineNumber,
|
|
62
|
+
column: match.column,
|
|
63
|
+
message: match.message,
|
|
64
|
+
severity: 'error',
|
|
65
|
+
code: trimmedLine,
|
|
66
|
+
type: match.type,
|
|
67
|
+
confidence: match.confidence,
|
|
68
|
+
suggestion: match.suggestion
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
return violations;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
isCommentOrImport(line) {
|
|
77
|
+
const trimmed = line.trim();
|
|
78
|
+
return trimmed.startsWith('//') ||
|
|
79
|
+
trimmed.startsWith('/*') ||
|
|
80
|
+
trimmed.startsWith('*') ||
|
|
81
|
+
trimmed.startsWith('import ') ||
|
|
82
|
+
trimmed.startsWith('export ');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
findSensitiveHardcode(line, originalLine) {
|
|
86
|
+
const matches = [];
|
|
87
|
+
|
|
88
|
+
// Skip template literals with variables - they are dynamic, not hardcoded
|
|
89
|
+
if (line.includes('${') || line.includes('`')) {
|
|
90
|
+
return matches;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Skip if line is clearly configuration, type definition, or UI-related
|
|
94
|
+
if (this.isConfigOrUIContext(line)) {
|
|
95
|
+
return matches;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Look for suspicious patterns with better context awareness
|
|
99
|
+
const patterns = [
|
|
100
|
+
{
|
|
101
|
+
name: 'suspicious_password_variable',
|
|
102
|
+
regex: /(const|let|var)\s+\w*[Pp]ass[Ww]ord\w*\s*=\s*['"`]([^'"`]{4,})['"`]/g,
|
|
103
|
+
severity: 'error',
|
|
104
|
+
message: 'Potential hardcoded password in variable assignment',
|
|
105
|
+
suggestion: 'Move sensitive values to environment variables or secure config files'
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
name: 'suspicious_secret_variable',
|
|
109
|
+
regex: /(const|let|var)\s+\w*[Ss]ecret\w*\s*=\s*['"`]([^'"`]{6,})['"`]/g,
|
|
110
|
+
severity: 'error',
|
|
111
|
+
message: 'Potential hardcoded secret in variable assignment',
|
|
112
|
+
suggestion: 'Use environment variables for secrets'
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
name: 'suspicious_short_password',
|
|
116
|
+
regex: /(const|let|var)\s+(?!use)\w*([Pp]ass|[Dd]b[Pp]ass|[Aa]dmin)(?!word[A-Z])\w*\s*=\s*['"`]([^'"`]{4,})['"`]/g,
|
|
117
|
+
severity: 'error',
|
|
118
|
+
message: 'Potential hardcoded password or admin credential',
|
|
119
|
+
suggestion: 'Use environment variables for credentials'
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: 'api_key',
|
|
123
|
+
regex: /(const|let|var)\s+\w*[Aa]pi[Kk]ey\w*\s*=\s*['"`]([^'"`]{10,})['"`]/g,
|
|
124
|
+
severity: 'error',
|
|
125
|
+
message: 'Potential hardcoded API key detected',
|
|
126
|
+
suggestion: 'Use environment variables for API keys'
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
name: 'auth_token',
|
|
130
|
+
regex: /(const|let|var)\s+\w*[Tt]oken\w*\s*=\s*['"`]([^'"`]{16,})['"`]/g,
|
|
131
|
+
severity: 'error',
|
|
132
|
+
message: 'Potential hardcoded authentication token detected',
|
|
133
|
+
suggestion: 'Store tokens in secure storage, not in source code'
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
name: 'database_url',
|
|
137
|
+
regex: /['"`](mongodb|mysql|postgres|redis):\/\/[^'"`]+['"`]/gi,
|
|
138
|
+
severity: 'error',
|
|
139
|
+
message: 'Hardcoded database connection string detected',
|
|
140
|
+
suggestion: 'Use environment variables for database connections'
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
name: 'suspicious_url',
|
|
144
|
+
regex: /['"`]https?:\/\/(?!localhost|127\.0\.0\.1|example\.com|test\.com|www\.w3\.org|www\.google\.com|googleapis\.com)[^'"`]{20,}['"`]/gi,
|
|
145
|
+
severity: 'warning',
|
|
146
|
+
message: 'Hardcoded external URL detected (consider configuration)',
|
|
147
|
+
suggestion: 'Consider moving URLs to configuration files'
|
|
148
|
+
}
|
|
149
|
+
];
|
|
150
|
+
|
|
151
|
+
// Additional context-aware checks
|
|
152
|
+
patterns.forEach(pattern => {
|
|
153
|
+
let match;
|
|
154
|
+
while ((match = pattern.regex.exec(line)) !== null) {
|
|
155
|
+
// Skip false positives
|
|
156
|
+
if (this.isFalsePositive(line, match[0], pattern.name)) {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
matches.push({
|
|
161
|
+
type: pattern.name,
|
|
162
|
+
column: match.index + 1,
|
|
163
|
+
message: pattern.message,
|
|
164
|
+
confidence: this.calculateConfidence(line, match[0], pattern.name),
|
|
165
|
+
suggestion: pattern.suggestion
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
return matches;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
isConfigOrUIContext(line) {
|
|
174
|
+
const lowerLine = line.toLowerCase();
|
|
175
|
+
|
|
176
|
+
// UI/Component contexts - likely false positives
|
|
177
|
+
const uiContexts = [
|
|
178
|
+
'inputtype', 'type:', 'type =', 'type:', 'inputtype=',
|
|
179
|
+
'routes =', 'route:', 'path:', 'routes:',
|
|
180
|
+
'import {', 'export {', 'from ', 'import ',
|
|
181
|
+
'interface', 'type ', 'enum ',
|
|
182
|
+
'props:', 'defaultprops',
|
|
183
|
+
'schema', 'validator',
|
|
184
|
+
'hook', 'use', 'const use', 'import.*use',
|
|
185
|
+
// React/UI specific
|
|
186
|
+
'textinput', 'input ', 'field ', 'form',
|
|
187
|
+
'component', 'page', 'screen', 'modal',
|
|
188
|
+
// Route/navigation specific
|
|
189
|
+
'navigation', 'route', 'path', 'url:', 'route:',
|
|
190
|
+
'setuppassword', 'resetpassword', 'forgotpassword',
|
|
191
|
+
'changepassword', 'confirmpassword'
|
|
192
|
+
];
|
|
193
|
+
|
|
194
|
+
return uiContexts.some(context => lowerLine.includes(context));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
isFalsePositive(line, matchedText, patternName) {
|
|
198
|
+
const lowerLine = line.toLowerCase();
|
|
199
|
+
const lowerMatch = matchedText.toLowerCase();
|
|
200
|
+
|
|
201
|
+
// Global false positive indicators
|
|
202
|
+
const globalFalsePositives = [
|
|
203
|
+
'test', 'mock', 'example', 'demo', 'sample', 'placeholder', 'dummy', 'fake',
|
|
204
|
+
'xmlns', 'namespace', 'schema', 'w3.org', 'google.com', 'googleapis.com',
|
|
205
|
+
'error', 'message', 'missing', 'invalid', 'failed'
|
|
206
|
+
];
|
|
207
|
+
|
|
208
|
+
// Check if the line contains any global false positive indicators
|
|
209
|
+
const hasGlobalFalsePositive = globalFalsePositives.some(pattern =>
|
|
210
|
+
lowerLine.includes(pattern) || lowerMatch.includes(pattern)
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
if (hasGlobalFalsePositive) {
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Common false positive patterns
|
|
218
|
+
const falsePositivePatterns = {
|
|
219
|
+
'suspicious_password_variable': [
|
|
220
|
+
'inputtype', 'type:', 'type =', 'activation', 'forgot_password', 'reset_password',
|
|
221
|
+
'setup_password', 'route', 'path', 'hook', 'use', 'change', 'confirm',
|
|
222
|
+
'validation', 'component', 'page', 'screen', 'textinput', 'input',
|
|
223
|
+
'trigger', 'useeffect', 'password.*trigger', 'renewpassword'
|
|
224
|
+
],
|
|
225
|
+
'suspicious_short_password': [
|
|
226
|
+
'inputtype', 'type:', 'type =', 'activation', 'forgot_password', 'reset_password',
|
|
227
|
+
'setup_password', 'route', 'path', 'hook', 'use', 'change', 'confirm',
|
|
228
|
+
'validation', 'component', 'page', 'screen', 'textinput'
|
|
229
|
+
],
|
|
230
|
+
'suspicious_secret_variable': [
|
|
231
|
+
'component', 'props', 'state', 'hook', 'use'
|
|
232
|
+
],
|
|
233
|
+
'suspicious_url': [
|
|
234
|
+
'localhost', '127.0.0.1', 'example.com', 'test.com', 'placeholder',
|
|
235
|
+
'mock', 'w3.org', 'google.com', 'recaptcha', 'googleapis.com'
|
|
236
|
+
],
|
|
237
|
+
'api_key': [
|
|
238
|
+
'test-', 'mock-', 'example-', 'demo-', 'missing', 'error', 'message'
|
|
239
|
+
]
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const patterns = falsePositivePatterns[patternName] || [];
|
|
243
|
+
|
|
244
|
+
// Check if line contains any pattern-specific false positive indicators
|
|
245
|
+
const hasPatternFalsePositive = patterns.some(pattern =>
|
|
246
|
+
lowerLine.includes(pattern) || lowerMatch.includes(pattern)
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
// Special handling for password-related patterns
|
|
250
|
+
if (patternName === 'hardcoded_password') {
|
|
251
|
+
// Allow if it's clearly UI/component related
|
|
252
|
+
if (lowerLine.includes('input') ||
|
|
253
|
+
lowerLine.includes('field') ||
|
|
254
|
+
lowerLine.includes('form') ||
|
|
255
|
+
lowerLine.includes('component') ||
|
|
256
|
+
lowerLine.includes('type') ||
|
|
257
|
+
lowerLine.includes('route') ||
|
|
258
|
+
lowerLine.includes('path') ||
|
|
259
|
+
lowerMatch.includes('activation') ||
|
|
260
|
+
lowerMatch.includes('forgot_password') ||
|
|
261
|
+
lowerMatch.includes('reset_password') ||
|
|
262
|
+
lowerMatch.includes('setup_password')) {
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return hasPatternFalsePositive;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
calculateConfidence(line, match, patternName) {
|
|
271
|
+
let confidence = 0.8; // Base confidence
|
|
272
|
+
|
|
273
|
+
// Reduce confidence for potential false positives
|
|
274
|
+
const lowerLine = line.toLowerCase();
|
|
275
|
+
|
|
276
|
+
if (lowerLine.includes('test') || lowerLine.includes('mock') || lowerLine.includes('example')) {
|
|
277
|
+
confidence -= 0.3;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (lowerLine.includes('const') || lowerLine.includes('let') || lowerLine.includes('var')) {
|
|
281
|
+
confidence += 0.1; // Variable assignments more likely to be hardcode
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (lowerLine.includes('type') || lowerLine.includes('component') || lowerLine.includes('props')) {
|
|
285
|
+
confidence -= 0.2; // UI-related less likely to be sensitive
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return Math.max(0.3, Math.min(1.0, confidence));
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
module.exports = new C041Analyzer();
|