@sun-asterisk/sunlint 1.3.0 → 1.3.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/CHANGELOG.md +115 -1
- package/CONTRIBUTING.md +249 -605
- package/README.md +3 -4
- package/config/ci-cd.json +54 -0
- package/config/development.json +56 -0
- package/config/large-project.json +143 -0
- package/config/presets/all.json +0 -1
- package/config/release.json +70 -0
- package/config/rule-analysis-strategies.js +38 -3
- package/config/rules/enhanced-rules-registry.json +474 -1179
- package/config/rules/rules-registry-generated.json +3 -3
- package/core/cli-action-handler.js +24 -30
- package/core/cli-program.js +11 -3
- package/core/config-merger.js +29 -2
- package/core/enhanced-rules-registry.js +3 -2
- package/core/semantic-engine.js +129 -19
- package/core/semantic-rule-base.js +4 -2
- package/core/unified-rule-registry.js +1 -1
- package/docs/COMMAND-EXAMPLES.md +134 -0
- package/docs/LARGE-PROJECT-GUIDE.md +324 -0
- package/engines/heuristic-engine.js +135 -16
- package/integrations/eslint/plugin/index.js +0 -2
- package/integrations/eslint/plugin/rules/common/c003-no-vague-abbreviations.js +59 -1
- package/integrations/eslint/plugin/rules/common/c006-function-name-verb-noun.js +26 -1
- package/integrations/eslint/plugin/rules/common/c030-use-custom-error-classes.js +54 -19
- package/origin-rules/common-en.md +19 -15
- package/package.json +1 -1
- package/rules/common/C002_no_duplicate_code/analyzer.js +334 -36
- package/rules/common/C003_no_vague_abbreviations/analyzer.js +220 -35
- package/rules/common/C006_function_naming/analyzer.js +29 -3
- package/rules/common/C010_limit_block_nesting/analyzer.js +181 -337
- package/rules/common/C010_limit_block_nesting/config.json +64 -0
- package/rules/common/C010_limit_block_nesting/regex-based-analyzer.js +379 -0
- package/rules/common/C010_limit_block_nesting/symbol-based-analyzer.js +231 -0
- package/rules/common/C013_no_dead_code/analyzer.js +75 -177
- package/rules/common/C013_no_dead_code/config.json +61 -0
- package/rules/common/C013_no_dead_code/regex-based-analyzer.js +345 -0
- package/rules/common/C013_no_dead_code/symbol-based-analyzer.js +640 -0
- package/rules/common/C014_dependency_injection/analyzer.js +48 -313
- package/rules/common/C014_dependency_injection/config.json +26 -0
- package/rules/common/C014_dependency_injection/symbol-based-analyzer.js +751 -0
- package/rules/common/C017_constructor_logic/analyzer.js +254 -17
- package/rules/common/C017_constructor_logic/semantic-analyzer.js +340 -0
- package/rules/common/C018_no_throw_generic_error/analyzer.js +232 -0
- package/rules/common/C018_no_throw_generic_error/config.json +50 -0
- package/rules/common/C018_no_throw_generic_error/regex-based-analyzer.js +387 -0
- package/rules/common/C018_no_throw_generic_error/symbol-based-analyzer.js +314 -0
- package/rules/common/C019_log_level_usage/analyzer.js +110 -317
- package/rules/common/C019_log_level_usage/pattern-analyzer.js +88 -0
- package/rules/common/C019_log_level_usage/system-log-analyzer.js +1267 -0
- package/rules/common/C023_no_duplicate_variable/analyzer.js +180 -0
- package/rules/common/C023_no_duplicate_variable/config.json +50 -0
- package/rules/common/C023_no_duplicate_variable/symbol-based-analyzer.js +158 -0
- package/rules/common/C024_no_scatter_hardcoded_constants/analyzer.js +180 -0
- package/rules/common/C024_no_scatter_hardcoded_constants/config.json +50 -0
- package/rules/common/C024_no_scatter_hardcoded_constants/symbol-based-analyzer.js +181 -0
- package/rules/common/C030_use_custom_error_classes/analyzer.js +200 -0
- package/rules/common/C033_separate_service_repository/README.md +78 -0
- package/rules/common/C033_separate_service_repository/analyzer.js +160 -0
- package/rules/common/C033_separate_service_repository/config.json +50 -0
- package/rules/common/C033_separate_service_repository/regex-based-analyzer.js +585 -0
- package/rules/common/C033_separate_service_repository/symbol-based-analyzer.js +368 -0
- package/rules/common/C035_error_logging_context/STRATEGY.md +99 -0
- package/rules/common/C035_error_logging_context/analyzer.js +232 -0
- package/rules/common/C035_error_logging_context/config.json +54 -0
- package/rules/common/C035_error_logging_context/regex-based-analyzer.js +299 -0
- package/rules/common/C035_error_logging_context/symbol-based-analyzer.js +454 -0
- package/rules/common/C040_centralized_validation/analyzer.js +165 -0
- package/rules/common/C040_centralized_validation/config.json +46 -0
- package/rules/common/C040_centralized_validation/regex-based-analyzer.js +243 -0
- package/rules/common/C040_centralized_validation/symbol-based-analyzer.js +416 -0
- package/rules/common/{C076_single_test_behavior → C072_single_test_behavior}/analyzer.js +6 -6
- package/rules/common/C076_explicit_function_types/README.md +30 -0
- package/rules/common/C076_explicit_function_types/analyzer.js +172 -0
- package/rules/common/C076_explicit_function_types/config.json +15 -0
- package/rules/common/C076_explicit_function_types/semantic-analyzer.js +341 -0
- package/rules/index.js +6 -1
- package/rules/parser/rule-parser.js +13 -2
- package/rules/security/S005_no_origin_auth/README.md +226 -0
- package/rules/security/S005_no_origin_auth/analyzer.js +184 -0
- package/rules/security/S005_no_origin_auth/ast-analyzer.js +406 -0
- package/rules/security/S005_no_origin_auth/config.json +85 -0
- package/rules/security/S006_no_plaintext_recovery_codes/README.md +139 -0
- package/rules/security/S006_no_plaintext_recovery_codes/analyzer.js +306 -0
- package/rules/security/S006_no_plaintext_recovery_codes/config.json +48 -0
- package/rules/security/S007_no_plaintext_otp/README.md +198 -0
- package/rules/security/S007_no_plaintext_otp/analyzer.js +406 -0
- package/rules/security/S007_no_plaintext_otp/config.json +79 -0
- package/rules/security/S007_no_plaintext_otp/semantic-analyzer.js +609 -0
- package/rules/security/S007_no_plaintext_otp/semantic-config.json +195 -0
- package/rules/security/S007_no_plaintext_otp/semantic-wrapper.js +280 -0
- package/rules/security/S009_no_insecure_encryption/README.md +158 -0
- package/rules/security/S009_no_insecure_encryption/analyzer.js +319 -0
- package/rules/security/S009_no_insecure_encryption/config.json +55 -0
- package/rules/security/S010_no_insecure_encryption/README.md +224 -0
- package/rules/security/S010_no_insecure_encryption/analyzer.js +493 -0
- package/rules/security/S010_no_insecure_encryption/config.json +48 -0
- package/rules/security/S016_no_sensitive_querystring/STRATEGY.md +149 -0
- package/rules/security/S016_no_sensitive_querystring/analyzer.js +276 -0
- package/rules/security/S016_no_sensitive_querystring/config.json +127 -0
- package/rules/security/S016_no_sensitive_querystring/regex-based-analyzer.js +258 -0
- package/rules/security/S016_no_sensitive_querystring/symbol-based-analyzer.js +495 -0
- package/rules/security/S027_no_hardcoded_secrets/analyzer.js +180 -366
- package/rules/security/S027_no_hardcoded_secrets/categories.json +153 -0
- package/rules/security/S027_no_hardcoded_secrets/categorized-analyzer.js +250 -0
- package/rules/security/S048_no_current_password_in_reset/README.md +222 -0
- package/rules/security/S048_no_current_password_in_reset/analyzer.js +366 -0
- package/rules/security/S048_no_current_password_in_reset/config.json +48 -0
- package/rules/security/S055_content_type_validation/README.md +176 -0
- package/rules/security/S055_content_type_validation/analyzer.js +312 -0
- package/rules/security/S055_content_type_validation/config.json +48 -0
- package/rules/utils/rule-helpers.js +140 -1
- package/scripts/consolidate-config.js +116 -0
- package/scripts/prepare-release.sh +1 -1
- package/config/rules/rules-registry.json +0 -765
- package/docs/ESLINT-INTEGRATION-STRATEGY.md +0 -392
- package/docs/FUTURE_PACKAGES.md +0 -83
- package/docs/HEURISTIC_VS_AI.md +0 -113
- package/docs/PRODUCTION_DEPLOYMENT_ANALYSIS.md +0 -112
- package/docs/PRODUCTION_SIZE_IMPACT.md +0 -183
- package/docs/RELEASE_GUIDE.md +0 -230
- package/docs/STANDARDIZED-CATEGORY-FILTERING.md +0 -156
- package/integrations/eslint/plugin/rules/common/c076-single-behavior-per-test.js +0 -254
- package/rules/common/C006_function_naming/smart-analyzer.js +0 -503
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* C035 Symbol-based Analyzer - Advanced Error Logging Context Analysis
|
|
3
|
+
* Purpose: Use AST + Symbol Resolution to analyze log content quality in catch blocks
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { SyntaxKind } = require('ts-morph');
|
|
7
|
+
|
|
8
|
+
class C035SymbolBasedAnalyzer {
|
|
9
|
+
constructor(semanticEngine = null) {
|
|
10
|
+
this.ruleId = 'C035';
|
|
11
|
+
this.ruleName = 'Error Logging Context Analysis (Symbol-Based)';
|
|
12
|
+
this.semanticEngine = semanticEngine;
|
|
13
|
+
this.verbose = false;
|
|
14
|
+
|
|
15
|
+
// Logger method patterns (extensible)
|
|
16
|
+
this.loggerPatterns = {
|
|
17
|
+
console: ['log', 'error', 'warn', 'info'],
|
|
18
|
+
logger: ['log', 'error', 'warn', 'info', 'debug'],
|
|
19
|
+
log: ['error', 'warn', 'info', 'debug'],
|
|
20
|
+
winston: ['log', 'error', 'warn', 'info', 'debug'],
|
|
21
|
+
bunyan: ['trace', 'debug', 'info', 'warn', 'error', 'fatal'],
|
|
22
|
+
pino: ['trace', 'debug', 'info', 'warn', 'error', 'fatal']
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Required context elements
|
|
26
|
+
this.requiredContext = {
|
|
27
|
+
errorInfo: ['message', 'stack', 'error', 'err'],
|
|
28
|
+
identifier: ['id', 'requestId', 'userId', 'transactionId', 'correlationId'],
|
|
29
|
+
context: ['service', 'method', 'operation', 'module', 'component']
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Sensitive data patterns to flag (more specific to avoid false positives)
|
|
33
|
+
this.sensitivePatterns = [
|
|
34
|
+
'password', 'passwd', 'pwd', 'pass',
|
|
35
|
+
'token', 'jwt', 'secret', 'privatekey', 'publickey', 'apikey', 'accesskey',
|
|
36
|
+
'ssn', 'social', 'creditcard', 'cardnumber', 'cvv', 'pin',
|
|
37
|
+
'authorization', 'bearer'
|
|
38
|
+
];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async initialize(semanticEngine = null) {
|
|
42
|
+
if (semanticEngine) {
|
|
43
|
+
this.semanticEngine = semanticEngine;
|
|
44
|
+
}
|
|
45
|
+
this.verbose = semanticEngine?.verbose || false;
|
|
46
|
+
|
|
47
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
48
|
+
console.log(`🔧 [C035 Symbol-Based] Analyzer initialized, verbose: ${this.verbose}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async analyzeFileBasic(filePath, options = {}) {
|
|
53
|
+
// This is the main entry point called by the hybrid analyzer
|
|
54
|
+
return await this.analyzeFileWithSymbols(filePath, options);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async analyzeFileWithSymbols(filePath, options = {}) {
|
|
58
|
+
const violations = [];
|
|
59
|
+
|
|
60
|
+
// Enable verbose mode if requested
|
|
61
|
+
const verbose = options.verbose || this.verbose;
|
|
62
|
+
|
|
63
|
+
if (!this.semanticEngine?.project) {
|
|
64
|
+
if (verbose) {
|
|
65
|
+
console.warn('[C035 Symbol-Based] No semantic engine available, skipping analysis');
|
|
66
|
+
}
|
|
67
|
+
return violations;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (verbose) {
|
|
71
|
+
console.log(`🔍 [C035 Symbol-Based] Starting analysis for ${filePath}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const sourceFile = this.semanticEngine.project.getSourceFile(filePath);
|
|
76
|
+
if (!sourceFile) {
|
|
77
|
+
return violations;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Find all try-catch statements in the file
|
|
81
|
+
const tryCatchStatements = sourceFile.getDescendantsOfKind(SyntaxKind.TryStatement);
|
|
82
|
+
|
|
83
|
+
if (verbose) {
|
|
84
|
+
console.log(`🔍 [C035 Symbol-Based] Found ${tryCatchStatements.length} try-catch statements`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
for (const tryStatement of tryCatchStatements) {
|
|
88
|
+
const catchClause = tryStatement.getCatchClause();
|
|
89
|
+
if (catchClause) {
|
|
90
|
+
const catchViolations = this.analyzeCatchBlock(catchClause, sourceFile, filePath, verbose);
|
|
91
|
+
violations.push(...catchViolations);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (verbose) {
|
|
96
|
+
console.log(`🔍 [C035 Symbol-Based] Total violations found: ${violations.length}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return violations;
|
|
100
|
+
} catch (error) {
|
|
101
|
+
if (verbose) {
|
|
102
|
+
console.warn(`[C035 Symbol-Based] Analysis failed for ${filePath}:`, error.message);
|
|
103
|
+
}
|
|
104
|
+
return violations;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Analyze catch block for logging context violations
|
|
110
|
+
*/
|
|
111
|
+
analyzeCatchBlock(catchClause, sourceFile, filePath, verbose = false) {
|
|
112
|
+
const violations = [];
|
|
113
|
+
|
|
114
|
+
if (verbose) {
|
|
115
|
+
console.log(`🔍 [C035 Symbol-Based] Analyzing catch block in ${filePath}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Get catch parameter (e, error, err, etc.)
|
|
119
|
+
const catchParameter = catchClause.getVariableDeclaration();
|
|
120
|
+
const errorVarName = catchParameter?.getName() || 'e';
|
|
121
|
+
|
|
122
|
+
if (verbose) {
|
|
123
|
+
console.log(`🔍 [C035 Symbol-Based] Error variable name: ${errorVarName}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Find all log calls within catch block
|
|
127
|
+
const catchBlock = catchClause.getBlock();
|
|
128
|
+
const logCalls = this.findLogCallsInBlock(catchBlock);
|
|
129
|
+
|
|
130
|
+
if (verbose) {
|
|
131
|
+
console.log(`🔍 [C035 Symbol-Based] Found ${logCalls.length} log calls in catch block`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (logCalls.length === 0) {
|
|
135
|
+
// No logging found - but this is C029's concern, not C035
|
|
136
|
+
// We only analyze existing logs for quality
|
|
137
|
+
return violations;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Analyze each log call for context quality
|
|
141
|
+
for (const logCall of logCalls) {
|
|
142
|
+
if (verbose) {
|
|
143
|
+
console.log(`🔍 [C035 Symbol-Based] Analyzing log call: ${logCall.getText()}`);
|
|
144
|
+
}
|
|
145
|
+
const logViolations = this.analyzeLogCall(logCall, errorVarName, sourceFile, filePath, verbose);
|
|
146
|
+
violations.push(...logViolations);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return violations;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Find all logging method calls within a block
|
|
154
|
+
*/
|
|
155
|
+
findLogCallsInBlock(block) {
|
|
156
|
+
const logCalls = [];
|
|
157
|
+
const callExpressions = block.getDescendantsOfKind(SyntaxKind.CallExpression);
|
|
158
|
+
|
|
159
|
+
for (const callExpr of callExpressions) {
|
|
160
|
+
if (this.isLoggerCall(callExpr)) {
|
|
161
|
+
logCalls.push(callExpr);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return logCalls;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Check if a call expression is a logger call
|
|
170
|
+
*/
|
|
171
|
+
isLoggerCall(callExpr) {
|
|
172
|
+
const expression = callExpr.getExpression();
|
|
173
|
+
|
|
174
|
+
// Handle property access (logger.error, console.log, etc.)
|
|
175
|
+
if (expression.getKind() === SyntaxKind.PropertyAccessExpression) {
|
|
176
|
+
const objectName = expression.getExpression().getText().toLowerCase();
|
|
177
|
+
const methodName = expression.getName().toLowerCase();
|
|
178
|
+
|
|
179
|
+
// Check against known logger patterns
|
|
180
|
+
for (const [loggerName, methods] of Object.entries(this.loggerPatterns)) {
|
|
181
|
+
if (objectName.includes(loggerName) && methods.includes(methodName)) {
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Analyze individual log call for context quality
|
|
192
|
+
*/
|
|
193
|
+
analyzeLogCall(logCall, errorVarName, sourceFile, filePath, verbose = false) {
|
|
194
|
+
const violations = [];
|
|
195
|
+
const lineNumber = logCall.getStartLineNumber();
|
|
196
|
+
const columnNumber = logCall.getStart() - logCall.getStartLinePos();
|
|
197
|
+
|
|
198
|
+
const args = logCall.getArguments();
|
|
199
|
+
if (args.length === 0) {
|
|
200
|
+
return violations; // No arguments to analyze
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Analyze logging structure and content
|
|
204
|
+
const analysis = this.analyzeLogArguments(args, errorVarName, verbose);
|
|
205
|
+
|
|
206
|
+
// Check for violations
|
|
207
|
+
if (!analysis.isStructured) {
|
|
208
|
+
violations.push({
|
|
209
|
+
ruleId: this.ruleId,
|
|
210
|
+
severity: 'warning',
|
|
211
|
+
message: 'Error logging should use structured format (object) instead of string concatenation',
|
|
212
|
+
source: this.ruleId,
|
|
213
|
+
file: filePath,
|
|
214
|
+
line: lineNumber,
|
|
215
|
+
column: columnNumber,
|
|
216
|
+
description: `[SYMBOL-BASED] Non-structured logging detected. Use object format for better parsing and monitoring.`,
|
|
217
|
+
suggestion: 'Use logger.error("message", { error: e.message, context: {...} }) instead of string concatenation',
|
|
218
|
+
category: 'logging'
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (!analysis.hasRequiredContext) {
|
|
223
|
+
violations.push({
|
|
224
|
+
ruleId: this.ruleId,
|
|
225
|
+
severity: 'warning',
|
|
226
|
+
message: 'Error logging missing required context information',
|
|
227
|
+
source: this.ruleId,
|
|
228
|
+
file: filePath,
|
|
229
|
+
line: lineNumber,
|
|
230
|
+
column: columnNumber,
|
|
231
|
+
description: `[SYMBOL-BASED] Missing context: ${analysis.missingContext.join(', ')}. Include identifiers and operation context.`,
|
|
232
|
+
suggestion: 'Add requestId, userId, and operation context to log for better traceability',
|
|
233
|
+
category: 'logging'
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (analysis.hasSensitiveData) {
|
|
238
|
+
violations.push({
|
|
239
|
+
ruleId: this.ruleId,
|
|
240
|
+
severity: 'error',
|
|
241
|
+
message: 'Error logging contains potentially sensitive data',
|
|
242
|
+
source: this.ruleId,
|
|
243
|
+
file: filePath,
|
|
244
|
+
line: lineNumber,
|
|
245
|
+
column: columnNumber,
|
|
246
|
+
description: `[SYMBOL-BASED] Sensitive patterns detected: ${analysis.sensitivePatterns.join(', ')}. Mask or exclude sensitive data.`,
|
|
247
|
+
suggestion: 'Mask sensitive data: password.substring(0,2) + "***" or exclude entirely',
|
|
248
|
+
category: 'security'
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return violations;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Analyze log arguments for structure, context, and sensitive data
|
|
257
|
+
*/
|
|
258
|
+
analyzeLogArguments(args, errorVarName, verbose = false) {
|
|
259
|
+
const analysis = {
|
|
260
|
+
isStructured: false,
|
|
261
|
+
hasRequiredContext: false,
|
|
262
|
+
hasSensitiveData: false,
|
|
263
|
+
missingContext: [],
|
|
264
|
+
sensitivePatterns: []
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
// Check if any argument is an object (structured logging)
|
|
268
|
+
for (const arg of args) {
|
|
269
|
+
if (arg.getKind() === SyntaxKind.ObjectLiteralExpression) {
|
|
270
|
+
analysis.isStructured = true;
|
|
271
|
+
|
|
272
|
+
// Analyze object properties for context and sensitive data
|
|
273
|
+
const properties = arg.getProperties();
|
|
274
|
+
this.analyzeObjectProperties(properties, analysis, verbose);
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// If not structured, check for string concatenation patterns
|
|
280
|
+
if (!analysis.isStructured) {
|
|
281
|
+
for (const arg of args) {
|
|
282
|
+
const argText = arg.getText().toLowerCase();
|
|
283
|
+
this.checkForSensitiveDataInText(argText, analysis);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Check required context
|
|
288
|
+
this.validateRequiredContext(analysis);
|
|
289
|
+
|
|
290
|
+
return analysis;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Analyze object literal properties for context and sensitive data
|
|
295
|
+
*/
|
|
296
|
+
analyzeObjectProperties(properties, analysis, verbose = false) {
|
|
297
|
+
const foundContext = {
|
|
298
|
+
errorInfo: false,
|
|
299
|
+
identifier: false,
|
|
300
|
+
context: false
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
if (verbose) {
|
|
304
|
+
console.log(`🔍 [C035 Symbol-Based] Analyzing ${properties.length} object properties`);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
for (const prop of properties) {
|
|
308
|
+
if (prop.getKind() === SyntaxKind.PropertyAssignment) {
|
|
309
|
+
const propName = prop.getName()?.toLowerCase() || '';
|
|
310
|
+
|
|
311
|
+
if (verbose) {
|
|
312
|
+
console.log(`🔍 [C035 Symbol-Based] Checking property: '${propName}' (kind: PropertyAssignment)`);
|
|
313
|
+
}
|
|
314
|
+
} else if (prop.getKind() === SyntaxKind.ShorthandPropertyAssignment) {
|
|
315
|
+
const propName = prop.getName()?.toLowerCase() || '';
|
|
316
|
+
|
|
317
|
+
if (verbose) {
|
|
318
|
+
console.log(`🔍 [C035 Symbol-Based] Checking shorthand property: '${propName}' (kind: ShorthandPropertyAssignment)`);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Check for required context - same logic for shorthand properties
|
|
322
|
+
if (this.requiredContext.errorInfo.some(ctx => propName.includes(ctx))) {
|
|
323
|
+
foundContext.errorInfo = true;
|
|
324
|
+
if (verbose) {
|
|
325
|
+
console.log(`🔍 [C035 Symbol-Based] Found error info in shorthand: '${propName}'`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (this.requiredContext.identifier.some(ctx => propName.includes(ctx))) {
|
|
329
|
+
foundContext.identifier = true;
|
|
330
|
+
if (verbose) {
|
|
331
|
+
console.log(`🔍 [C035 Symbol-Based] Found identifier in shorthand: '${propName}'`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (this.requiredContext.context.some(ctx => propName.includes(ctx))) {
|
|
335
|
+
foundContext.context = true;
|
|
336
|
+
if (verbose) {
|
|
337
|
+
console.log(`🔍 [C035 Symbol-Based] Found context in shorthand: '${propName}'`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Check for sensitive data in shorthand properties too
|
|
342
|
+
const matchingSensitivePattern = this.sensitivePatterns.find(pattern => {
|
|
343
|
+
const regex = new RegExp(`\\b${pattern}\\b`, 'i');
|
|
344
|
+
return regex.test(propName);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
if (matchingSensitivePattern) {
|
|
348
|
+
analysis.hasSensitiveData = true;
|
|
349
|
+
analysis.sensitivePatterns.push(propName);
|
|
350
|
+
if (verbose) {
|
|
351
|
+
console.log(`🔍 [C035 Symbol-Based] Sensitive pattern detected in shorthand: '${propName}' matches '${matchingSensitivePattern}'`);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
} else {
|
|
355
|
+
if (verbose) {
|
|
356
|
+
console.log(`🔍 [C035 Symbol-Based] Skipping property with kind: ${prop.getKindName()}`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Original PropertyAssignment logic
|
|
361
|
+
if (prop.getKind() === SyntaxKind.PropertyAssignment) {
|
|
362
|
+
const propName = prop.getName()?.toLowerCase() || '';
|
|
363
|
+
|
|
364
|
+
if (verbose) {
|
|
365
|
+
console.log(`🔍 [C035 Symbol-Based] Checking property: '${propName}'`);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Check for required context
|
|
369
|
+
if (this.requiredContext.errorInfo.some(ctx => propName.includes(ctx))) {
|
|
370
|
+
foundContext.errorInfo = true;
|
|
371
|
+
if (verbose) {
|
|
372
|
+
console.log(`🔍 [C035 Symbol-Based] Found error info: '${propName}'`);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
if (this.requiredContext.identifier.some(ctx => propName.includes(ctx))) {
|
|
376
|
+
foundContext.identifier = true;
|
|
377
|
+
if (verbose) {
|
|
378
|
+
console.log(`🔍 [C035 Symbol-Based] Found identifier: '${propName}'`);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
if (this.requiredContext.context.some(ctx => propName.includes(ctx))) {
|
|
382
|
+
foundContext.context = true;
|
|
383
|
+
if (verbose) {
|
|
384
|
+
console.log(`🔍 [C035 Symbol-Based] Found context: '${propName}'`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Check for sensitive data (use word boundaries to avoid false positives)
|
|
389
|
+
const matchingSensitivePattern = this.sensitivePatterns.find(pattern => {
|
|
390
|
+
const regex = new RegExp(`\\b${pattern}\\b`, 'i');
|
|
391
|
+
return regex.test(propName);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
if (matchingSensitivePattern) {
|
|
395
|
+
analysis.hasSensitiveData = true;
|
|
396
|
+
analysis.sensitivePatterns.push(propName);
|
|
397
|
+
if (verbose) {
|
|
398
|
+
console.log(`🔍 [C035 Symbol-Based] Sensitive pattern detected: '${propName}' matches '${matchingSensitivePattern}'`);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Update analysis based on found context
|
|
405
|
+
// For structured logs, be more lenient - having error info is sufficient
|
|
406
|
+
// Identifier and context are nice-to-have but not required for structured logs
|
|
407
|
+
analysis.hasRequiredContext = foundContext.errorInfo ||
|
|
408
|
+
(foundContext.identifier && foundContext.context);
|
|
409
|
+
|
|
410
|
+
if (verbose) {
|
|
411
|
+
console.log(`🔍 [C035 Symbol-Based] Context found - errorInfo: ${foundContext.errorInfo}, identifier: ${foundContext.identifier}, context: ${foundContext.context}`);
|
|
412
|
+
console.log(`🔍 [C035 Symbol-Based] hasRequiredContext: ${analysis.hasRequiredContext}`);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Only flag missing context if there's no error info at all
|
|
416
|
+
// For structured logs with error object, consider it sufficient
|
|
417
|
+
if (!foundContext.errorInfo && !foundContext.identifier) {
|
|
418
|
+
analysis.missingContext.push('error information or identifier');
|
|
419
|
+
}
|
|
420
|
+
// Remove the overly strict context requirement for structured logs with error info
|
|
421
|
+
// Context is nice-to-have but not required when we have structured error info
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Check text for sensitive data patterns
|
|
426
|
+
*/
|
|
427
|
+
checkForSensitiveDataInText(text, analysis) {
|
|
428
|
+
for (const pattern of this.sensitivePatterns) {
|
|
429
|
+
if (text.includes(pattern)) {
|
|
430
|
+
analysis.hasSensitiveData = true;
|
|
431
|
+
analysis.sensitivePatterns.push(pattern);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Validate required context elements
|
|
438
|
+
*/
|
|
439
|
+
validateRequiredContext(analysis) {
|
|
440
|
+
// For structured logs, context validation is already handled in analyzeObjectProperties
|
|
441
|
+
if (analysis.isStructured) {
|
|
442
|
+
// If structured and has error info, consider it sufficient
|
|
443
|
+
if (analysis.hasRequiredContext && analysis.missingContext.length === 0) {
|
|
444
|
+
return; // Already validated in analyzeObjectProperties
|
|
445
|
+
}
|
|
446
|
+
} else {
|
|
447
|
+
// For non-structured logs, we can't reliably detect context
|
|
448
|
+
analysis.missingContext.push('structured format required for context validation');
|
|
449
|
+
analysis.hasRequiredContext = false;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
module.exports = C035SymbolBasedAnalyzer;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* C040 Main Analyzer - Symbol-based with minimal regex fallback
|
|
3
|
+
* Primary: Symbol-based analysis (95% cases)
|
|
4
|
+
* Fallback: Regex-based only when symbol analysis completely fails
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const C040SymbolBasedAnalyzer = require('./symbol-based-analyzer');
|
|
8
|
+
const C040RegexBasedAnalyzer = require('./regex-based-analyzer');
|
|
9
|
+
|
|
10
|
+
class C040Analyzer {
|
|
11
|
+
constructor(options = {}) {
|
|
12
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
13
|
+
console.log(`🔧 [C040] Constructor called with options:`, !!options);
|
|
14
|
+
console.log(`🔧 [C040] Options type:`, typeof options, Object.keys(options || {}));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
this.ruleId = 'C040';
|
|
18
|
+
this.ruleName = 'Centralized Validation Logic';
|
|
19
|
+
this.description = 'Don\'t scatter validation logic across multiple classes - Move validation to dedicated validators';
|
|
20
|
+
this.semanticEngine = options.semanticEngine || null;
|
|
21
|
+
this.verbose = options.verbose || false;
|
|
22
|
+
|
|
23
|
+
// Initialize analyzers
|
|
24
|
+
this.symbolBasedAnalyzer = new C040SymbolBasedAnalyzer(this.semanticEngine);
|
|
25
|
+
this.regexBasedAnalyzer = new C040RegexBasedAnalyzer(this.semanticEngine);
|
|
26
|
+
|
|
27
|
+
// Configuration
|
|
28
|
+
this.config = {
|
|
29
|
+
useSymbolBased: true, // Primary approach
|
|
30
|
+
fallbackToRegex: true, // Only when symbol fails completely
|
|
31
|
+
symbolBasedOnly: false // Can be set to true for pure mode
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Initialize with semantic engine
|
|
37
|
+
*/
|
|
38
|
+
async initialize(semanticEngine = null) {
|
|
39
|
+
if (semanticEngine) {
|
|
40
|
+
this.semanticEngine = semanticEngine;
|
|
41
|
+
}
|
|
42
|
+
this.verbose = semanticEngine?.verbose || false;
|
|
43
|
+
|
|
44
|
+
// Initialize both analyzers
|
|
45
|
+
await this.symbolBasedAnalyzer.initialize(semanticEngine);
|
|
46
|
+
await this.regexBasedAnalyzer.initialize(semanticEngine);
|
|
47
|
+
|
|
48
|
+
if (this.verbose) {
|
|
49
|
+
console.log(`[DEBUG] 🔧 C040: Analyzer initialized - Symbol-based: ✅, Regex fallback: ${this.config.fallbackToRegex ? '✅' : '❌'}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async analyze(files, language, options = {}) {
|
|
54
|
+
const violations = [];
|
|
55
|
+
let symbolCount = 0;
|
|
56
|
+
let regexCount = 0;
|
|
57
|
+
|
|
58
|
+
for (const filePath of files) {
|
|
59
|
+
try {
|
|
60
|
+
const fileViolations = await this.analyzeFile(filePath, options);
|
|
61
|
+
violations.push(...fileViolations);
|
|
62
|
+
|
|
63
|
+
// Count strategy usage
|
|
64
|
+
const strategy = fileViolations[0]?.analysisStrategy;
|
|
65
|
+
if (strategy === 'symbol-based') symbolCount++;
|
|
66
|
+
else if (strategy === 'regex-fallback') regexCount++;
|
|
67
|
+
|
|
68
|
+
} catch (error) {
|
|
69
|
+
if (this.verbose) {
|
|
70
|
+
console.warn(`[C040] Analysis failed for ${filePath}:`, error.message);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Summary of strategy usage
|
|
76
|
+
if (this.verbose && (symbolCount > 0 || regexCount > 0)) {
|
|
77
|
+
console.log(`📊 [C040-SUMMARY] Analysis strategy usage:`);
|
|
78
|
+
console.log(` 🧠 Symbol-based: ${symbolCount} files`);
|
|
79
|
+
console.log(` 🔄 Regex-fallback: ${regexCount} files`);
|
|
80
|
+
console.log(` 📈 Coverage: ${symbolCount}/${symbolCount + regexCount} files used primary strategy`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return violations;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async analyzeFile(filePath, options = {}) {
|
|
87
|
+
// 1. Try Symbol-based analysis first (primary)
|
|
88
|
+
if (this.config.useSymbolBased && this.semanticEngine?.project) {
|
|
89
|
+
try {
|
|
90
|
+
const sourceFile = this.semanticEngine.project.getSourceFile(filePath);
|
|
91
|
+
if (sourceFile) {
|
|
92
|
+
const violations = await this.symbolBasedAnalyzer.analyzeFile(filePath, options);
|
|
93
|
+
|
|
94
|
+
if (this.verbose) {
|
|
95
|
+
console.log(`🧠 [C040-SYMBOL] ${filePath}: Found ${violations.length} violations`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return violations.map(v => ({ ...v, analysisStrategy: 'symbol-based' }));
|
|
99
|
+
} else {
|
|
100
|
+
if (this.verbose) {
|
|
101
|
+
console.log(`⚠️ [C040-SYMBOL] ${filePath}: Source file not found in ts-morph project, falling back to regex`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
} catch (error) {
|
|
105
|
+
if (this.verbose) {
|
|
106
|
+
console.warn(`❌ [C040-SYMBOL] ${filePath}: Symbol analysis failed, falling back to regex:`, error.message);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
if (this.verbose) {
|
|
111
|
+
const reason = !this.config.useSymbolBased ? 'Symbol-based disabled' : 'No semantic engine';
|
|
112
|
+
console.log(`⚠️ [C040] ${filePath}: Skipping symbol analysis (${reason}), using regex`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 2. Fallback to Regex-based analysis (only if symbol fails or unavailable)
|
|
117
|
+
if (this.config.fallbackToRegex && !this.config.symbolBasedOnly) {
|
|
118
|
+
try {
|
|
119
|
+
const violations = await this.regexBasedAnalyzer.analyzeFileBasic(filePath, options);
|
|
120
|
+
|
|
121
|
+
if (this.verbose) {
|
|
122
|
+
console.log(`🔄 [C040-REGEX] ${filePath}: Found ${violations.length} violations`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return violations.map(v => ({ ...v, analysisStrategy: 'regex-fallback' }));
|
|
126
|
+
} catch (error) {
|
|
127
|
+
if (this.verbose) {
|
|
128
|
+
console.warn(`❌ [C040-REGEX] ${filePath}: Regex fallback also failed:`, error.message);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return [];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Legacy compatibility methods
|
|
137
|
+
async analyzeWithSemantics(filePath, options = {}) {
|
|
138
|
+
return await this.analyzeFile(filePath, options);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async analyzeFileBasic(filePath, options = {}) {
|
|
142
|
+
// Force regex-based for legacy compatibility
|
|
143
|
+
const violations = await this.regexBasedAnalyzer.analyzeFileBasic(filePath, options);
|
|
144
|
+
return violations.map(v => ({ ...v, analysisStrategy: 'regex-legacy' }));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Configuration methods
|
|
148
|
+
enableSymbolBasedOnly() {
|
|
149
|
+
this.config.symbolBasedOnly = true;
|
|
150
|
+
this.config.fallbackToRegex = false;
|
|
151
|
+
if (this.verbose) {
|
|
152
|
+
console.log(`[C040] Switched to symbol-based only mode`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
enableHybridMode() {
|
|
157
|
+
this.config.symbolBasedOnly = false;
|
|
158
|
+
this.config.fallbackToRegex = true;
|
|
159
|
+
if (this.verbose) {
|
|
160
|
+
console.log(`[C040] Switched to hybrid mode (symbol-based + regex fallback)`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
module.exports = C040Analyzer;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"rule": "C040",
|
|
3
|
+
"name": "Centralized Validation Logic",
|
|
4
|
+
"description": "Don't scatter validation logic across multiple classes",
|
|
5
|
+
"category": "code-quality",
|
|
6
|
+
"languages": ["javascript", "typescript"],
|
|
7
|
+
"applies_to": ["all"],
|
|
8
|
+
"strategies": ["symbol-based", "regex"],
|
|
9
|
+
"priority": "hybrid",
|
|
10
|
+
"severity": "major",
|
|
11
|
+
"enabled": true,
|
|
12
|
+
"options": {
|
|
13
|
+
"minCentralizationScore": 70,
|
|
14
|
+
"maxDuplicationCount": 3,
|
|
15
|
+
"frameworkDetection": true,
|
|
16
|
+
"layerDetection": true,
|
|
17
|
+
"validationPatterns": [
|
|
18
|
+
"validate*",
|
|
19
|
+
"*Validator",
|
|
20
|
+
"isValid*",
|
|
21
|
+
"ensure*",
|
|
22
|
+
"check*"
|
|
23
|
+
],
|
|
24
|
+
"frameworkKeywords": [
|
|
25
|
+
"zod",
|
|
26
|
+
"joi",
|
|
27
|
+
"yup",
|
|
28
|
+
"class-validator",
|
|
29
|
+
"ajv",
|
|
30
|
+
"checkSchema",
|
|
31
|
+
"validateSync"
|
|
32
|
+
],
|
|
33
|
+
"layerPatterns": {
|
|
34
|
+
"controller": ["**/controllers/**", "**/controller/**", "**/*Controller*", "**/*controller*"],
|
|
35
|
+
"service": ["**/services/**", "**/service/**", "**/*Service*", "**/*service*"],
|
|
36
|
+
"repository": ["**/repositories/**", "**/repository/**", "**/*Repository*", "**/*repository*"],
|
|
37
|
+
"validator": ["**/validators/**", "**/validation/**", "**/*Validator*", "**/*validator*"]
|
|
38
|
+
},
|
|
39
|
+
"errorTypes": [
|
|
40
|
+
"ValidationError",
|
|
41
|
+
"BadRequest",
|
|
42
|
+
"InvalidInput",
|
|
43
|
+
"TypeError"
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
}
|