@sun-asterisk/sunlint 1.2.1 → 1.2.2
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/config/rule-analysis-strategies.js +18 -2
- package/engines/eslint-engine.js +9 -11
- package/engines/heuristic-engine.js +55 -31
- package/package.json +2 -1
- 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-backup.js +426 -0
- package/rules/common/C029_catch_block_logging/analyzer-fixed.js +130 -0
- package/rules/common/C029_catch_block_logging/analyzer-multi-tech.js +487 -0
- package/rules/common/C029_catch_block_logging/analyzer-simple.js +110 -0
- package/rules/common/C029_catch_block_logging/analyzer-smart-pipeline.js +755 -0
- package/rules/common/C029_catch_block_logging/analyzer.js +129 -0
- package/rules/common/C029_catch_block_logging/ast-analyzer-backup.js +441 -0
- package/rules/common/C029_catch_block_logging/ast-analyzer-new.js +127 -0
- package/rules/common/C029_catch_block_logging/ast-analyzer.js +133 -0
- package/rules/common/C029_catch_block_logging/cfg-analyzer.js +408 -0
- package/rules/common/C029_catch_block_logging/config.json +59 -0
- package/rules/common/C029_catch_block_logging/dataflow-analyzer.js +454 -0
- package/rules/common/C029_catch_block_logging/multi-language-ast-engine.js +700 -0
- package/rules/common/C029_catch_block_logging/pattern-learning-analyzer.js +568 -0
- package/rules/common/C029_catch_block_logging/semantic-analyzer.js +459 -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/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 +155 -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/universal/C010/generic.js +0 -0
- package/rules/universal/C010/tree-sitter-analyzer.js +0 -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/generate_insights.js +188 -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
|
@@ -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();
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
class C041ASTAnalyzer {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.ruleId = 'C041';
|
|
7
|
+
this.ruleName = 'No Hardcoded Sensitive Information (AST-Enhanced)';
|
|
8
|
+
this.description = 'AST-based detection of hardcoded sensitive information - superior to regex approach';
|
|
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 AST 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.analyzeJSTS(filePath, content, config);
|
|
36
|
+
default:
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async analyzeJSTS(filePath, content, config) {
|
|
42
|
+
const violations = [];
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
// Try AST analysis first (like ESLint approach)
|
|
46
|
+
const astViolations = await this.analyzeWithAST(filePath, content, config);
|
|
47
|
+
if (astViolations.length > 0) {
|
|
48
|
+
violations.push(...astViolations);
|
|
49
|
+
}
|
|
50
|
+
} catch (astError) {
|
|
51
|
+
if (config.verbose) {
|
|
52
|
+
console.log(`⚠️ AST analysis failed for ${path.basename(filePath)}, falling back to regex`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Fallback to regex-based analysis
|
|
56
|
+
const regexViolations = await this.analyzeWithRegex(filePath, content, config);
|
|
57
|
+
violations.push(...regexViolations);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return violations;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async analyzeWithAST(filePath, content, config) {
|
|
64
|
+
const violations = [];
|
|
65
|
+
|
|
66
|
+
// Import AST modules dynamically
|
|
67
|
+
let astModules;
|
|
68
|
+
try {
|
|
69
|
+
astModules = require('../../../core/ast-modules');
|
|
70
|
+
} catch (error) {
|
|
71
|
+
throw new Error('AST modules not available');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Try to parse with AST
|
|
75
|
+
let ast;
|
|
76
|
+
try {
|
|
77
|
+
// Use the registry's parseCode method
|
|
78
|
+
ast = await astModules.parseCode(content, 'javascript', filePath);
|
|
79
|
+
if (!ast) {
|
|
80
|
+
throw new Error('AST parsing returned null');
|
|
81
|
+
}
|
|
82
|
+
} catch (parseError) {
|
|
83
|
+
throw new Error(`Parse error: ${parseError.message}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Traverse AST to find sensitive information - mimicking ESLint's approach
|
|
87
|
+
const rootNode = ast.program || ast; // Handle both Babel and ESLint formats
|
|
88
|
+
this.traverseAST(rootNode, (node) => {
|
|
89
|
+
if (this.isLiteralNode(node)) {
|
|
90
|
+
const violation = this.checkLiteralForSensitiveInfo(node, filePath, content);
|
|
91
|
+
if (violation) {
|
|
92
|
+
violations.push(violation);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (this.isTemplateLiteralNode(node)) {
|
|
97
|
+
const violation = this.checkTemplateLiteralForSensitiveInfo(node, filePath, content);
|
|
98
|
+
if (violation) {
|
|
99
|
+
violations.push(violation);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
return violations;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
traverseAST(node, callback) {
|
|
108
|
+
if (!node || typeof node !== 'object') return;
|
|
109
|
+
|
|
110
|
+
callback(node);
|
|
111
|
+
|
|
112
|
+
for (const key in node) {
|
|
113
|
+
if (key === 'parent' || key === 'leadingComments' || key === 'trailingComments') continue;
|
|
114
|
+
|
|
115
|
+
const child = node[key];
|
|
116
|
+
if (Array.isArray(child)) {
|
|
117
|
+
child.forEach(item => this.traverseAST(item, callback));
|
|
118
|
+
} else if (child && typeof child === 'object') {
|
|
119
|
+
this.traverseAST(child, callback);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
isLiteralNode(node) {
|
|
125
|
+
// Support both ESLint format (Literal) and Babel format (StringLiteral)
|
|
126
|
+
return node && (node.type === 'Literal' || node.type === 'StringLiteral') && typeof node.value === 'string';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
isTemplateLiteralNode(node) {
|
|
130
|
+
return node && node.type === 'TemplateLiteral' &&
|
|
131
|
+
node.quasis && node.quasis.length === 1 && // No variable interpolation
|
|
132
|
+
node.expressions && node.expressions.length === 0;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
checkLiteralForSensitiveInfo(node, filePath, content) {
|
|
136
|
+
const value = node.value;
|
|
137
|
+
if (!value || value.length < 4) return null;
|
|
138
|
+
|
|
139
|
+
const lines = content.split('\n');
|
|
140
|
+
const lineNumber = node.loc.start.line;
|
|
141
|
+
const lineText = lines[lineNumber - 1] || '';
|
|
142
|
+
|
|
143
|
+
// Skip if it's in UI/component context - same as ESLint
|
|
144
|
+
if (this.isFalsePositive(value, lineText)) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Check against sensitive patterns - enhanced version of ESLint patterns
|
|
149
|
+
const sensitivePattern = this.detectSensitivePattern(value, lineText);
|
|
150
|
+
if (sensitivePattern) {
|
|
151
|
+
return {
|
|
152
|
+
ruleId: this.ruleId,
|
|
153
|
+
file: filePath,
|
|
154
|
+
line: lineNumber,
|
|
155
|
+
column: node.loc.start.column + 1,
|
|
156
|
+
message: sensitivePattern.message,
|
|
157
|
+
severity: 'warning', // Match ESLint severity
|
|
158
|
+
code: lineText.trim(),
|
|
159
|
+
type: sensitivePattern.type,
|
|
160
|
+
confidence: sensitivePattern.confidence,
|
|
161
|
+
suggestion: sensitivePattern.suggestion
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
checkTemplateLiteralForSensitiveInfo(node, filePath, content) {
|
|
169
|
+
if (!node.quasis || node.quasis.length !== 1) return null;
|
|
170
|
+
|
|
171
|
+
const value = node.quasis[0].value.raw;
|
|
172
|
+
if (!value || value.length < 4) return null;
|
|
173
|
+
|
|
174
|
+
// Create a mock literal node for consistent processing
|
|
175
|
+
const mockNode = {
|
|
176
|
+
...node,
|
|
177
|
+
value: value,
|
|
178
|
+
loc: node.loc
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
return this.checkLiteralForSensitiveInfo(mockNode, filePath, content);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
detectSensitivePattern(value, lineText) {
|
|
185
|
+
const lowerValue = value.toLowerCase();
|
|
186
|
+
const lowerLine = lineText.toLowerCase();
|
|
187
|
+
|
|
188
|
+
// Enhanced patterns based on ESLint rule but with better detection
|
|
189
|
+
const sensitivePatterns = [
|
|
190
|
+
{
|
|
191
|
+
type: 'password',
|
|
192
|
+
condition: () => /password/i.test(lineText) && value.length >= 4,
|
|
193
|
+
message: 'Potential hardcoded sensitive information detected. Move sensitive values to environment variables or secure config files.',
|
|
194
|
+
confidence: 0.8,
|
|
195
|
+
suggestion: 'Move sensitive values to environment variables or secure config files'
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
type: 'secret',
|
|
199
|
+
condition: () => /secret/i.test(lineText) && value.length >= 6,
|
|
200
|
+
message: 'Potential hardcoded sensitive information detected. Move sensitive values to environment variables or secure config files.',
|
|
201
|
+
confidence: 0.8,
|
|
202
|
+
suggestion: 'Use environment variables for secrets'
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
type: 'api_key',
|
|
206
|
+
condition: () => /api[_-]?key/i.test(lineText) && value.length >= 10,
|
|
207
|
+
message: 'Potential hardcoded sensitive information detected. Move sensitive values to environment variables or secure config files.',
|
|
208
|
+
confidence: 0.9,
|
|
209
|
+
suggestion: 'Use environment variables for API keys'
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
type: 'auth_token',
|
|
213
|
+
condition: () => /auth[_-]?token/i.test(lineText) && value.length >= 16,
|
|
214
|
+
message: 'Potential hardcoded sensitive information detected. Move sensitive values to environment variables or secure config files.',
|
|
215
|
+
confidence: 0.9,
|
|
216
|
+
suggestion: 'Store tokens in secure storage'
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
type: 'access_token',
|
|
220
|
+
condition: () => /access[_-]?token/i.test(lineText) && value.length >= 16,
|
|
221
|
+
message: 'Potential hardcoded sensitive information detected. Move sensitive values to environment variables or secure config files.',
|
|
222
|
+
confidence: 0.9,
|
|
223
|
+
suggestion: 'Store tokens in secure storage'
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
type: 'database_url',
|
|
227
|
+
condition: () => /(mongodb|mysql|postgres|redis):\/\//i.test(value) && value.length >= 10,
|
|
228
|
+
message: 'Potential hardcoded sensitive information detected. Move sensitive values to environment variables or secure config files.',
|
|
229
|
+
confidence: 0.95,
|
|
230
|
+
suggestion: 'Use environment variables for database connections'
|
|
231
|
+
}
|
|
232
|
+
];
|
|
233
|
+
|
|
234
|
+
for (const pattern of sensitivePatterns) {
|
|
235
|
+
if (pattern.condition()) {
|
|
236
|
+
return pattern;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
isFalsePositive(value, sourceCode) {
|
|
244
|
+
const lowerValue = value.toLowerCase();
|
|
245
|
+
const lowerLine = sourceCode.toLowerCase();
|
|
246
|
+
|
|
247
|
+
// Global false positive indicators - same as ESLint
|
|
248
|
+
const globalFalsePositives = [
|
|
249
|
+
'test', 'mock', 'example', 'demo', 'sample', 'placeholder', 'dummy', 'fake',
|
|
250
|
+
'xmlns', 'namespace', 'schema', 'w3.org', 'google.com', 'googleapis.com',
|
|
251
|
+
'error', 'message', 'missing', 'invalid', 'failed', 'localhost', '127.0.0.1'
|
|
252
|
+
];
|
|
253
|
+
|
|
254
|
+
// Check global false positives
|
|
255
|
+
if (globalFalsePositives.some(pattern => lowerValue.includes(pattern))) {
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Check if line context suggests UI/component usage - same as ESLint
|
|
260
|
+
if (this.isConfigOrUIContext(lowerLine)) {
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
isConfigOrUIContext(line) {
|
|
268
|
+
// Same logic as ESLint rule
|
|
269
|
+
const uiContexts = [
|
|
270
|
+
'inputtype', 'type:', 'type =', 'inputtype=',
|
|
271
|
+
'routes =', 'route:', 'path:', 'routes:',
|
|
272
|
+
'import {', 'export {', 'from ', 'import ',
|
|
273
|
+
'interface', 'type ', 'enum ',
|
|
274
|
+
'props:', 'defaultprops',
|
|
275
|
+
'schema', 'validator',
|
|
276
|
+
'hook', 'use', 'const use', 'import.*use',
|
|
277
|
+
// React/UI specific
|
|
278
|
+
'textinput', 'input ', 'field ', 'form',
|
|
279
|
+
'component', 'page', 'screen', 'modal',
|
|
280
|
+
// Route/navigation specific
|
|
281
|
+
'navigation', 'route', 'path', 'url:', 'route:',
|
|
282
|
+
'setuppassword', 'resetpassword', 'forgotpassword',
|
|
283
|
+
'changepassword', 'confirmpassword'
|
|
284
|
+
];
|
|
285
|
+
|
|
286
|
+
return uiContexts.some(context => line.includes(context));
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async analyzeWithRegex(filePath, content, config) {
|
|
290
|
+
// Fallback to original regex approach if AST fails
|
|
291
|
+
const originalAnalyzer = require('./analyzer.js');
|
|
292
|
+
return originalAnalyzer.analyzeTypeScript(filePath, content, config);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
module.exports = new C041ASTAnalyzer();
|