@sun-asterisk/sunlint 1.3.4 → 1.3.5
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 +32 -0
- package/config/presets/all.json +49 -48
- package/config/presets/beginner.json +7 -18
- package/config/presets/ci.json +63 -27
- package/config/presets/maintainability.json +6 -4
- package/config/presets/performance.json +4 -3
- package/config/presets/quality.json +11 -50
- package/config/presets/recommended.json +83 -10
- package/config/presets/security.json +20 -19
- package/config/presets/strict.json +6 -13
- package/config/rules/enhanced-rules-registry.json +64 -7
- package/core/config-preset-resolver.js +7 -2
- package/package.json +1 -1
- package/rules/common/C067_no_hardcoded_config/analyzer.js +95 -0
- package/rules/common/C067_no_hardcoded_config/config.json +81 -0
- package/rules/common/C067_no_hardcoded_config/symbol-based-analyzer.js +1000 -0
- package/rules/security/S024_xpath_xxe_protection/analyzer.js +242 -0
- package/rules/security/S024_xpath_xxe_protection/config.json +152 -0
- package/rules/security/S024_xpath_xxe_protection/regex-based-analyzer.js +338 -0
- package/rules/security/S024_xpath_xxe_protection/symbol-based-analyzer.js +474 -0
- package/rules/security/S025_server_side_validation/README.md +179 -0
- package/rules/security/S025_server_side_validation/analyzer.js +242 -0
- package/rules/security/S025_server_side_validation/config.json +111 -0
- package/rules/security/S025_server_side_validation/regex-based-analyzer.js +388 -0
- package/rules/security/S025_server_side_validation/symbol-based-analyzer.js +523 -0
- package/scripts/README.md +83 -0
- package/scripts/analyze-core-rules.js +151 -0
- package/scripts/generate-presets.js +202 -0
|
@@ -0,0 +1,1000 @@
|
|
|
1
|
+
// rules/common/C067_no_hardcoded_config/symbol-based-analyzer.js
|
|
2
|
+
const { SyntaxKind, Project, Node } = require('ts-morph');
|
|
3
|
+
|
|
4
|
+
class C067SymbolBasedAnalyzer {
|
|
5
|
+
constructor(semanticEngine = null) {
|
|
6
|
+
this.semanticEngine = semanticEngine;
|
|
7
|
+
this.verbose = false;
|
|
8
|
+
|
|
9
|
+
// Common UI/framework strings that should be excluded
|
|
10
|
+
this.UI_STRINGS = [
|
|
11
|
+
'checkbox', 'button', 'search', 'remove', 'submit', 'cancel', 'ok', 'close',
|
|
12
|
+
'Authorization', 'User-Agent', 'Content-Type', 'Accept', 'Bearer',
|
|
13
|
+
'ArrowDown', 'ArrowUp', 'ArrowLeft', 'ArrowRight', 'bottom', 'top', 'left', 'right',
|
|
14
|
+
'next-auth/react', '@nestjs/swagger', '@nestjs/common', 'nestjs-pino'
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
// Test-related strings to exclude
|
|
18
|
+
this.TEST_PATTERNS = [
|
|
19
|
+
/^(test|mock|example|dummy|placeholder|fixture|stub)/i,
|
|
20
|
+
/^(User \d+|Test User|Admin User)/i,
|
|
21
|
+
/^(group\d+|item\d+|element\d+)/i,
|
|
22
|
+
/^(abcdef\d+|123456|test-\w+)/i
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
// Configuration patterns to detect - focused on environment-dependent config
|
|
26
|
+
this.configPatterns = {
|
|
27
|
+
// API endpoints and URLs - only external URLs, not internal endpoints
|
|
28
|
+
urls: {
|
|
29
|
+
regex: /^https?:\/\/(?!localhost|127\.0\.0\.1|0\.0\.0\.0)([a-zA-Z0-9-]+\.[a-zA-Z]{2,}|[^\/\s]+\.[^\/\s]+)(\/[^\s]*)?$/,
|
|
30
|
+
exclude: [
|
|
31
|
+
/^https?:\/\/(localhost|127\.0\.0\.1|0\.0\.0\.0)(:\d+)?/, // Local development
|
|
32
|
+
/^https?:\/\/(example\.com|test\.com|dummy\.com)/, // Test domains
|
|
33
|
+
/^(http|https):\/\/\$\{.+\}/ // Template URLs with variables
|
|
34
|
+
]
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
// Environment-dependent numeric values (ports, timeouts that differ by env)
|
|
38
|
+
environmentNumbers: {
|
|
39
|
+
// Only consider numbers that are commonly different between environments
|
|
40
|
+
isEnvironmentDependent: (value, context) => {
|
|
41
|
+
const lowerContext = context.toLowerCase();
|
|
42
|
+
|
|
43
|
+
// Business logic numbers are NOT environment config
|
|
44
|
+
const businessLogicPatterns = [
|
|
45
|
+
/limit|max|min|size|count|length|threshold/i,
|
|
46
|
+
/page|record|item|batch|chunk|export/i,
|
|
47
|
+
/width|height|margin|padding/i,
|
|
48
|
+
/attempt|retry|step/i
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
if (businessLogicPatterns.some(pattern => pattern.test(context))) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Very specific values that are usually business constants
|
|
56
|
+
const businessConstants = [
|
|
57
|
+
20000, 10000, 5000, 1000, 500, 100, 50, 20, 10, 5, // Common limits
|
|
58
|
+
404, 500, 200, 201, 400, 401, 403, // HTTP status codes
|
|
59
|
+
24, 60, 3600, 86400, // Time constants (hours, minutes, seconds)
|
|
60
|
+
1, 2, 3, 4, 5, 6, 7, 8, 9, 10 // Simple counters
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
if (businessConstants.includes(value)) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Port numbers (except common ones like 80, 443, 3000, 8080)
|
|
68
|
+
if (typeof value === 'number' && value > 1000 && value < 65536) {
|
|
69
|
+
const commonPorts = [3000, 8000, 8080, 9000, 5000, 4200, 4000];
|
|
70
|
+
if (!commonPorts.includes(value)) {
|
|
71
|
+
// Check if context suggests it's a port
|
|
72
|
+
return /port|listen|bind|server/i.test(context);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Large timeout values that might differ by environment (> 10 seconds)
|
|
77
|
+
if (typeof value === 'number' && value > 10000) {
|
|
78
|
+
return /timeout|interval|delay|duration/i.test(context) &&
|
|
79
|
+
!businessLogicPatterns.some(pattern => pattern.test(context));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
// Database and connection strings
|
|
87
|
+
connections: {
|
|
88
|
+
regex: /^(mongodb|mysql|postgres|redis|elasticsearch):\/\/|^jdbc:|^Server=|^Data Source=/i
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
// API Keys and tokens (but exclude validation messages)
|
|
92
|
+
credentials: {
|
|
93
|
+
keywords: ['apikey', 'api_key', 'secret_key', 'access_token', 'client_secret'],
|
|
94
|
+
exclude: [
|
|
95
|
+
/must contain|should contain|invalid|error|message/i, // Validation messages
|
|
96
|
+
/description|comment|note/i, // Descriptions
|
|
97
|
+
/^[a-z\s]{10,}$/i // Long descriptive text
|
|
98
|
+
]
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async initialize(semanticEngine = null) {
|
|
104
|
+
if (semanticEngine) {
|
|
105
|
+
this.semanticEngine = semanticEngine;
|
|
106
|
+
}
|
|
107
|
+
this.verbose = semanticEngine?.verbose || false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async analyzeFileBasic(filePath, options = {}) {
|
|
111
|
+
const violations = [];
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
const sourceFile = this.semanticEngine.project.getSourceFile(filePath);
|
|
115
|
+
if (!sourceFile) {
|
|
116
|
+
if (this.verbose) {
|
|
117
|
+
console.log(`[DEBUG] 🔍 C067: File not in semantic project, trying standalone: ${filePath.split('/').pop()}`);
|
|
118
|
+
}
|
|
119
|
+
// Fallback to standalone analysis if file not in semantic project
|
|
120
|
+
return await this.analyzeFileStandalone(filePath, options);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (this.verbose) {
|
|
124
|
+
console.log(`[DEBUG] 🔍 C067: Analyzing hardcoded config in ${filePath.split('/').pop()}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Skip test files and config files themselves
|
|
128
|
+
if (this.isConfigOrTestFile(filePath)) {
|
|
129
|
+
if (this.verbose) {
|
|
130
|
+
console.log(`[DEBUG] 🔍 C067: Skipping config/test file: ${filePath.split('/').pop()}`);
|
|
131
|
+
}
|
|
132
|
+
return violations;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Find hardcoded configuration values
|
|
136
|
+
const hardcodedConfigs = this.findHardcodedConfigs(sourceFile);
|
|
137
|
+
|
|
138
|
+
for (const config of hardcodedConfigs) {
|
|
139
|
+
violations.push({
|
|
140
|
+
ruleId: 'C067',
|
|
141
|
+
message: this.createMessage(config),
|
|
142
|
+
filePath: filePath,
|
|
143
|
+
line: config.line,
|
|
144
|
+
column: config.column,
|
|
145
|
+
severity: 'warning',
|
|
146
|
+
category: 'configuration',
|
|
147
|
+
type: config.type,
|
|
148
|
+
value: config.value,
|
|
149
|
+
suggestion: this.getSuggestion(config.type)
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (this.verbose) {
|
|
154
|
+
console.log(`[DEBUG] 🔍 C067: Found ${violations.length} hardcoded config violations`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return violations;
|
|
158
|
+
} catch (error) {
|
|
159
|
+
if (this.verbose) {
|
|
160
|
+
console.error(`[DEBUG] ❌ C067: Symbol analysis error: ${error.message}`);
|
|
161
|
+
}
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async analyzeFileStandalone(filePath, options = {}) {
|
|
167
|
+
const violations = [];
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
// Create a standalone ts-morph project for this analysis
|
|
171
|
+
const project = new Project({
|
|
172
|
+
compilerOptions: {
|
|
173
|
+
target: 'ES2020',
|
|
174
|
+
module: 'CommonJS',
|
|
175
|
+
allowJs: true,
|
|
176
|
+
allowSyntheticDefaultImports: true,
|
|
177
|
+
esModuleInterop: true,
|
|
178
|
+
skipLibCheck: true,
|
|
179
|
+
strict: false
|
|
180
|
+
},
|
|
181
|
+
useInMemoryFileSystem: true
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Add the source file to the project
|
|
185
|
+
const fs = require('fs');
|
|
186
|
+
const path = require('path');
|
|
187
|
+
|
|
188
|
+
// Check if file exists first
|
|
189
|
+
if (!fs.existsSync(filePath)) {
|
|
190
|
+
throw new Error(`File not found on filesystem: ${filePath}`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Read file content and create source file
|
|
194
|
+
const fileContent = fs.readFileSync(filePath, 'utf8');
|
|
195
|
+
const fileName = path.basename(filePath);
|
|
196
|
+
const sourceFile = project.createSourceFile(fileName, fileContent);
|
|
197
|
+
|
|
198
|
+
if (!sourceFile) {
|
|
199
|
+
throw new Error(`Source file not found: ${filePath}`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (this.verbose) {
|
|
203
|
+
console.log(`[DEBUG] 🔍 C067: Analyzing hardcoded config in ${filePath.split('/').pop()} (standalone)`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Skip test files and config files themselves
|
|
207
|
+
if (this.isConfigOrTestFile(filePath)) {
|
|
208
|
+
if (this.verbose) {
|
|
209
|
+
console.log(`[DEBUG] 🔍 C067: Skipping config/test file: ${filePath.split('/').pop()}`);
|
|
210
|
+
}
|
|
211
|
+
return violations;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Find hardcoded configuration values
|
|
215
|
+
const hardcodedConfigs = this.findHardcodedConfigs(sourceFile);
|
|
216
|
+
|
|
217
|
+
for (const config of hardcodedConfigs) {
|
|
218
|
+
violations.push({
|
|
219
|
+
ruleId: 'C067',
|
|
220
|
+
message: this.createMessage(config),
|
|
221
|
+
filePath: filePath,
|
|
222
|
+
line: config.line,
|
|
223
|
+
column: config.column,
|
|
224
|
+
severity: 'warning',
|
|
225
|
+
category: 'configuration',
|
|
226
|
+
type: config.type,
|
|
227
|
+
value: config.value,
|
|
228
|
+
suggestion: this.getSuggestion(config.type)
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (this.verbose) {
|
|
233
|
+
console.log(`[DEBUG] 🔍 C067: Found ${violations.length} hardcoded config violations (standalone)`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Clean up the project
|
|
237
|
+
project.removeSourceFile(sourceFile);
|
|
238
|
+
|
|
239
|
+
return violations;
|
|
240
|
+
} catch (error) {
|
|
241
|
+
if (this.verbose) {
|
|
242
|
+
console.error(`[DEBUG] ❌ C067: Standalone analysis error: ${error.message}`);
|
|
243
|
+
}
|
|
244
|
+
throw error;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
isConfigOrTestFile(filePath) {
|
|
249
|
+
// Skip config files themselves and test files (NOT dummy files used in production)
|
|
250
|
+
const fileName = filePath.toLowerCase();
|
|
251
|
+
const configPatterns = [
|
|
252
|
+
/config\.(ts|js|json)$/,
|
|
253
|
+
/\.config\.(ts|js)$/,
|
|
254
|
+
/\.env$/,
|
|
255
|
+
/\.env\./,
|
|
256
|
+
/constants\.(ts|js)$/,
|
|
257
|
+
/settings\.(ts|js)$/,
|
|
258
|
+
/defaults\.(ts|js)$/
|
|
259
|
+
];
|
|
260
|
+
|
|
261
|
+
const testPatterns = [
|
|
262
|
+
/\.(test|spec)\.(ts|tsx|js|jsx)$/,
|
|
263
|
+
/\/__tests__\//,
|
|
264
|
+
/\/test\//,
|
|
265
|
+
/\/tests\//,
|
|
266
|
+
/\.stories\.(ts|tsx|js|jsx)$/,
|
|
267
|
+
/\.mock\.(ts|tsx|js|jsx)$/
|
|
268
|
+
// NOTE: Deliberately NOT including /dummy/ because dummy files
|
|
269
|
+
// in production code often contain hardcoded config that should be flagged
|
|
270
|
+
];
|
|
271
|
+
|
|
272
|
+
return configPatterns.some(pattern => pattern.test(fileName)) ||
|
|
273
|
+
testPatterns.some(pattern => pattern.test(fileName));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
findHardcodedConfigs(sourceFile) {
|
|
277
|
+
const configs = [];
|
|
278
|
+
|
|
279
|
+
// Traverse all nodes in the source file
|
|
280
|
+
sourceFile.forEachDescendant((node) => {
|
|
281
|
+
// Check string literals
|
|
282
|
+
if (node.getKind() === SyntaxKind.StringLiteral) {
|
|
283
|
+
const config = this.analyzeStringLiteral(node, sourceFile);
|
|
284
|
+
if (config) {
|
|
285
|
+
configs.push(config);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Check numeric literals
|
|
290
|
+
if (node.getKind() === SyntaxKind.NumericLiteral) {
|
|
291
|
+
const config = this.analyzeNumericLiteral(node, sourceFile);
|
|
292
|
+
if (config) {
|
|
293
|
+
configs.push(config);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Check template literals (for URLs with variables)
|
|
298
|
+
if (node.getKind() === SyntaxKind.TemplateExpression) {
|
|
299
|
+
const config = this.analyzeTemplateLiteral(node, sourceFile);
|
|
300
|
+
if (config) {
|
|
301
|
+
configs.push(config);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Check property assignments
|
|
306
|
+
if (node.getKind() === SyntaxKind.PropertyAssignment) {
|
|
307
|
+
const config = this.analyzePropertyAssignment(node, sourceFile);
|
|
308
|
+
if (config) {
|
|
309
|
+
configs.push(config);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Check variable declarations
|
|
314
|
+
if (node.getKind() === SyntaxKind.VariableDeclaration) {
|
|
315
|
+
const config = this.analyzeVariableDeclaration(node, sourceFile);
|
|
316
|
+
if (config) {
|
|
317
|
+
configs.push(config);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
return configs;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
analyzeStringLiteral(node, sourceFile) {
|
|
326
|
+
const value = node.getLiteralValue();
|
|
327
|
+
const position = sourceFile.getLineAndColumnAtPos(node.getStart());
|
|
328
|
+
|
|
329
|
+
// Skip short strings and common UI values
|
|
330
|
+
if (value.length < 4) return null;
|
|
331
|
+
|
|
332
|
+
const parentContext = this.getParentContext(node);
|
|
333
|
+
|
|
334
|
+
// Skip import paths and module names
|
|
335
|
+
if (this.isImportPath(value, node)) return null;
|
|
336
|
+
|
|
337
|
+
// Skip UI strings and labels
|
|
338
|
+
if (this.isUIString(value)) return null;
|
|
339
|
+
|
|
340
|
+
// Skip test data and mocks
|
|
341
|
+
if (this.isTestData(value, parentContext)) return null;
|
|
342
|
+
|
|
343
|
+
// Skip validation messages and error messages
|
|
344
|
+
if (this.isValidationMessage(value, parentContext)) return null;
|
|
345
|
+
|
|
346
|
+
// Skip file names and descriptions
|
|
347
|
+
if (this.isFileNameOrDescription(value, parentContext)) return null;
|
|
348
|
+
|
|
349
|
+
// Skip config keys (like 'api.baseUrl', 'features.newUI', etc.)
|
|
350
|
+
if (this.looksLikeConfigKey(value)) {
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Skip if this is used in a config service call
|
|
355
|
+
if (parentContext.includes('config.get') || parentContext.includes('config.getString') ||
|
|
356
|
+
parentContext.includes('config.getBoolean') || parentContext.includes('config.getNumber')) {
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Skip if this is a property key in an object literal
|
|
361
|
+
if (this.isPropertyKey(node)) {
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Check for environment-dependent URLs only
|
|
366
|
+
if (this.configPatterns.urls.regex.test(value)) {
|
|
367
|
+
if (!this.isExcludedUrl(value, node) && this.isEnvironmentDependentUrl(value)) {
|
|
368
|
+
return {
|
|
369
|
+
type: 'url',
|
|
370
|
+
value: value,
|
|
371
|
+
line: position.line,
|
|
372
|
+
column: position.column,
|
|
373
|
+
node: node
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Check for real credentials (not validation messages)
|
|
379
|
+
if (this.isRealCredential(value, parentContext)) {
|
|
380
|
+
return {
|
|
381
|
+
type: 'credential',
|
|
382
|
+
value: value,
|
|
383
|
+
line: position.line,
|
|
384
|
+
column: position.column,
|
|
385
|
+
node: node,
|
|
386
|
+
context: parentContext
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Check for connection strings
|
|
391
|
+
if (this.configPatterns.connections.regex.test(value)) {
|
|
392
|
+
return {
|
|
393
|
+
type: 'connection',
|
|
394
|
+
value: value,
|
|
395
|
+
line: position.line,
|
|
396
|
+
column: position.column,
|
|
397
|
+
node: node
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
analyzeNumericLiteral(node, sourceFile) {
|
|
405
|
+
const value = node.getLiteralValue();
|
|
406
|
+
const position = sourceFile.getLineAndColumnAtPos(node.getStart());
|
|
407
|
+
const parentContext = this.getParentContext(node);
|
|
408
|
+
|
|
409
|
+
// Only check for environment-dependent numbers
|
|
410
|
+
if (this.configPatterns.environmentNumbers.isEnvironmentDependent(value, parentContext)) {
|
|
411
|
+
return {
|
|
412
|
+
type: 'environment_config',
|
|
413
|
+
value: value,
|
|
414
|
+
line: position.line,
|
|
415
|
+
column: position.column,
|
|
416
|
+
node: node,
|
|
417
|
+
context: parentContext
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
analyzeTemplateLiteral(node, sourceFile) {
|
|
425
|
+
// For now, focus on simple template literals that might contain URLs
|
|
426
|
+
const templateText = node.getFullText();
|
|
427
|
+
if (templateText.includes('http://') || templateText.includes('https://')) {
|
|
428
|
+
const position = sourceFile.getLineAndColumnAtPos(node.getStart());
|
|
429
|
+
|
|
430
|
+
// Check if it's using environment variables or config
|
|
431
|
+
if (!templateText.includes('process.env') && !templateText.includes('config.')) {
|
|
432
|
+
return {
|
|
433
|
+
type: 'template_url',
|
|
434
|
+
value: templateText.trim(),
|
|
435
|
+
line: position.line,
|
|
436
|
+
column: position.column,
|
|
437
|
+
node: node
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
analyzePropertyAssignment(node, sourceFile) {
|
|
446
|
+
const nameNode = node.getNameNode();
|
|
447
|
+
const valueNode = node.getInitializer();
|
|
448
|
+
|
|
449
|
+
if (!nameNode || !valueNode) return null;
|
|
450
|
+
|
|
451
|
+
const propertyName = nameNode.getText();
|
|
452
|
+
const position = sourceFile.getLineAndColumnAtPos(node.getStart());
|
|
453
|
+
|
|
454
|
+
// Skip field mapping objects
|
|
455
|
+
const ancestorObj = node.getParent();
|
|
456
|
+
if (ancestorObj && Node.isObjectLiteralExpression(ancestorObj)) {
|
|
457
|
+
const objParent = ancestorObj.getParent();
|
|
458
|
+
if (objParent && Node.isVariableDeclaration(objParent)) {
|
|
459
|
+
const varName = objParent.getName();
|
|
460
|
+
if (/mapping|map|field|column|decode/i.test(varName)) {
|
|
461
|
+
return null; // Skip field mapping objects
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Skip common business logic properties that aren't environment-dependent
|
|
467
|
+
const businessLogicProperties = [
|
|
468
|
+
'endpoint', 'path', 'route', // API routing
|
|
469
|
+
'limit', 'pageSize', 'batchSize', // Pagination (usually business logic)
|
|
470
|
+
'retry', 'retries', 'maxRetries', // Retry logic (usually business logic)
|
|
471
|
+
'count', 'max', 'min' // Common limits
|
|
472
|
+
];
|
|
473
|
+
|
|
474
|
+
const lowerPropertyName = propertyName.toLowerCase();
|
|
475
|
+
if (businessLogicProperties.some(prop => lowerPropertyName.includes(prop))) {
|
|
476
|
+
// Only flag if it's clearly environment-dependent
|
|
477
|
+
let value = null;
|
|
478
|
+
if (valueNode.getKind() === SyntaxKind.StringLiteral) {
|
|
479
|
+
value = valueNode.getLiteralValue();
|
|
480
|
+
// Only flag URLs or clearly environment-dependent strings
|
|
481
|
+
if (!this.configPatterns.urls.regex.test(value) || !this.isEnvironmentDependentUrl(value)) {
|
|
482
|
+
return null;
|
|
483
|
+
}
|
|
484
|
+
} else if (valueNode.getKind() === SyntaxKind.NumericLiteral) {
|
|
485
|
+
value = valueNode.getLiteralValue();
|
|
486
|
+
const parentContext = this.getParentContext(node);
|
|
487
|
+
// Only flag if it's clearly environment-dependent (like ports, large timeouts)
|
|
488
|
+
if (!this.configPatterns.environmentNumbers.isEnvironmentDependent(value, parentContext)) {
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Check if property name suggests environment-dependent configuration
|
|
495
|
+
if (this.isEnvironmentDependentProperty(propertyName)) {
|
|
496
|
+
let value = null;
|
|
497
|
+
|
|
498
|
+
if (valueNode.getKind() === SyntaxKind.StringLiteral) {
|
|
499
|
+
value = valueNode.getLiteralValue();
|
|
500
|
+
} else if (valueNode.getKind() === SyntaxKind.NumericLiteral) {
|
|
501
|
+
value = valueNode.getLiteralValue();
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (value !== null && this.looksLikeEnvironmentConfig(propertyName, value)) {
|
|
505
|
+
return {
|
|
506
|
+
type: 'property_config',
|
|
507
|
+
value: value,
|
|
508
|
+
line: position.line,
|
|
509
|
+
column: position.column,
|
|
510
|
+
node: node,
|
|
511
|
+
propertyName: propertyName
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
return null;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
analyzeVariableDeclaration(node, sourceFile) {
|
|
520
|
+
const nameNode = node.getNameNode();
|
|
521
|
+
const initializer = node.getInitializer();
|
|
522
|
+
|
|
523
|
+
if (!nameNode || !initializer) return null;
|
|
524
|
+
|
|
525
|
+
const variableName = nameNode.getText();
|
|
526
|
+
const position = sourceFile.getLineAndColumnAtPos(node.getStart());
|
|
527
|
+
|
|
528
|
+
// Check if variable name suggests environment-dependent configuration
|
|
529
|
+
if (this.isEnvironmentDependentProperty(variableName)) {
|
|
530
|
+
let value = null;
|
|
531
|
+
|
|
532
|
+
if (initializer.getKind() === SyntaxKind.StringLiteral) {
|
|
533
|
+
value = initializer.getLiteralValue();
|
|
534
|
+
} else if (initializer.getKind() === SyntaxKind.NumericLiteral) {
|
|
535
|
+
value = initializer.getLiteralValue();
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (value !== null && this.looksLikeEnvironmentConfig(variableName, value)) {
|
|
539
|
+
return {
|
|
540
|
+
type: 'variable_config',
|
|
541
|
+
value: value,
|
|
542
|
+
line: position.line,
|
|
543
|
+
column: position.column,
|
|
544
|
+
node: node,
|
|
545
|
+
variableName: variableName
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
return null;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
getParentContext(node) {
|
|
554
|
+
// Get surrounding context to understand the purpose of the literal
|
|
555
|
+
let parent = node.getParent();
|
|
556
|
+
let context = '';
|
|
557
|
+
|
|
558
|
+
// Check if this is a method call argument or property access
|
|
559
|
+
while (parent && context.length < 100) {
|
|
560
|
+
const parentText = parent.getText();
|
|
561
|
+
|
|
562
|
+
// If parent is CallExpression and this node is an argument, it might be a config key
|
|
563
|
+
if (parent.getKind() === SyntaxKind.CallExpression) {
|
|
564
|
+
const callExpr = parent;
|
|
565
|
+
const methodName = this.getMethodName(callExpr);
|
|
566
|
+
if (['get', 'getBoolean', 'getNumber', 'getArray', 'getString'].includes(methodName)) {
|
|
567
|
+
return `config.${methodName}()`; // This indicates it's a config key
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
if (parentText.length < 200) {
|
|
572
|
+
context = parentText;
|
|
573
|
+
break;
|
|
574
|
+
}
|
|
575
|
+
parent = parent.getParent();
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
return context;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
getMethodName(callExpression) {
|
|
582
|
+
const expression = callExpression.getExpression();
|
|
583
|
+
if (expression.getKind() === SyntaxKind.PropertyAccessExpression) {
|
|
584
|
+
return expression.getName();
|
|
585
|
+
}
|
|
586
|
+
if (expression.getKind() === SyntaxKind.Identifier) {
|
|
587
|
+
return expression.getText();
|
|
588
|
+
}
|
|
589
|
+
return '';
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
isExcludedUrl(value, node) {
|
|
593
|
+
return this.configPatterns.urls.exclude.some(pattern => pattern.test(value));
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
isExcludedCredential(value, node) {
|
|
597
|
+
return this.configPatterns.credentials.exclude.some(pattern => pattern.test(value));
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
containsCredentialKeyword(context) {
|
|
601
|
+
const lowerContext = context.toLowerCase();
|
|
602
|
+
|
|
603
|
+
// Skip if this looks like a header name or property key definition
|
|
604
|
+
if (context.includes("':") || context.includes('": ') || context.includes(' = ')) {
|
|
605
|
+
// This might be a key-value pair where the string is the key
|
|
606
|
+
return false;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
return this.configPatterns.credentials.keywords.some(keyword =>
|
|
610
|
+
lowerContext.includes(keyword)
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
looksLikeUIValue(value, context) {
|
|
615
|
+
// Check if it's likely a UI-related value (like input type, label, etc.)
|
|
616
|
+
const uiKeywords = ['input', 'type', 'field', 'label', 'placeholder', 'text', 'button'];
|
|
617
|
+
const lowerContext = context.toLowerCase();
|
|
618
|
+
return uiKeywords.some(keyword => lowerContext.includes(keyword));
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
looksLikeConfigKey(value) {
|
|
622
|
+
// Check if it looks like a config key path (e.g., 'api.baseUrl', 'features.newUI')
|
|
623
|
+
if (/^[a-zA-Z][a-zA-Z0-9]*\.[a-zA-Z][a-zA-Z0-9]*(\.[a-zA-Z][a-zA-Z0-9]*)*$/.test(value)) {
|
|
624
|
+
return true;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Check for other config key patterns
|
|
628
|
+
const configKeyPatterns = [
|
|
629
|
+
/^[a-zA-Z][a-zA-Z0-9]*\.[a-zA-Z]/, // dotted notation like 'api.url'
|
|
630
|
+
/^[A-Z_][A-Z0-9_]*$/, // CONSTANT_CASE like 'API_URL'
|
|
631
|
+
/^get[A-Z]/, // getter methods like 'getApiUrl'
|
|
632
|
+
/^config\./, // config namespace
|
|
633
|
+
/^settings\./, // settings namespace
|
|
634
|
+
/^env\./ // env namespace
|
|
635
|
+
];
|
|
636
|
+
|
|
637
|
+
return configKeyPatterns.some(pattern => pattern.test(value));
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
isPropertyKey(node) {
|
|
641
|
+
// Check if this string literal is used as a property key in an object literal
|
|
642
|
+
const parent = node.getParent();
|
|
643
|
+
|
|
644
|
+
// If parent is PropertyAssignment and this node is the name, it's a property key
|
|
645
|
+
if (parent && parent.getKind() === SyntaxKind.PropertyAssignment) {
|
|
646
|
+
const nameNode = parent.getNameNode();
|
|
647
|
+
return nameNode === node;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
return false;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
isImportPath(value, node) {
|
|
654
|
+
// Check if this is likely an import path or module name
|
|
655
|
+
const parent = node.getParent();
|
|
656
|
+
|
|
657
|
+
// Check if it's in an import statement
|
|
658
|
+
let currentNode = parent;
|
|
659
|
+
while (currentNode) {
|
|
660
|
+
const kind = currentNode.getKind();
|
|
661
|
+
if (kind === SyntaxKind.ImportDeclaration ||
|
|
662
|
+
kind === SyntaxKind.ExportDeclaration ||
|
|
663
|
+
kind === SyntaxKind.CallExpression) {
|
|
664
|
+
const text = currentNode.getText();
|
|
665
|
+
if (text.includes('require(') || text.includes('import ') || text.includes('from ')) {
|
|
666
|
+
return true;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
currentNode = currentNode.getParent();
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Check for common import path patterns
|
|
673
|
+
return /^[@a-z][a-z0-9\-_]*\/|^[a-z][a-z0-9\-_]*$|^\.{1,2}\//.test(value) ||
|
|
674
|
+
value.endsWith('.js') || value.endsWith('.ts') ||
|
|
675
|
+
value.endsWith('.json') || value.endsWith('.css') ||
|
|
676
|
+
value.endsWith('.scss') || value.endsWith('.html');
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
isUIString(value) {
|
|
680
|
+
// Check against predefined UI string patterns, but don't skip credentials
|
|
681
|
+
if (typeof value === 'string' && value.length > 20 &&
|
|
682
|
+
(/token|key|secret|bearer|auth/i.test(value) || /^[a-f0-9-]{30,}$/i.test(value))) {
|
|
683
|
+
// Don't skip potential credentials/tokens even if they contain UI keywords
|
|
684
|
+
return false;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
return this.UI_STRINGS.some(pattern => {
|
|
688
|
+
if (typeof pattern === 'string') {
|
|
689
|
+
return value === pattern; // Exact match only, not includes
|
|
690
|
+
} else {
|
|
691
|
+
return pattern.test(value);
|
|
692
|
+
}
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
isTestData(value, context) {
|
|
697
|
+
// Don't skip credentials/tokens even in dummy files
|
|
698
|
+
if (typeof value === 'string' && value.length > 15 &&
|
|
699
|
+
(/token|key|secret|auth|bearer|jwt/i.test(value) ||
|
|
700
|
+
/^[a-f0-9-]{20,}$/i.test(value) || // Hex tokens
|
|
701
|
+
/^[A-Za-z0-9_-]{20,}$/i.test(value))) { // Base64-like tokens
|
|
702
|
+
return false; // Don't skip potential credentials
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// Check for test patterns in value - but be more restrictive
|
|
706
|
+
if (this.TEST_PATTERNS.some(pattern => pattern.test(value))) {
|
|
707
|
+
// Only skip if it's clearly test data, not production dummy data
|
|
708
|
+
const isInTestFile = /\.(test|spec)\.(ts|tsx|js|jsx)$/i.test(context) ||
|
|
709
|
+
/\/__tests__\//i.test(context) ||
|
|
710
|
+
/\/test\//i.test(context);
|
|
711
|
+
return isInTestFile;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// Check for test context
|
|
715
|
+
const lowerContext = context.toLowerCase();
|
|
716
|
+
const testKeywords = ['test', 'spec', 'mock', 'fixture', 'stub', 'describe', 'it('];
|
|
717
|
+
return testKeywords.some(keyword => lowerContext.includes(keyword));
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
isValidationMessage(value, context) {
|
|
721
|
+
// Skip validation/error messages
|
|
722
|
+
const validationPatterns = [
|
|
723
|
+
/must contain|should contain|invalid|error|required|missing/i,
|
|
724
|
+
/password|username|email/i, // Common validation contexts
|
|
725
|
+
/^[A-Z][a-z\s]{10,}$/, // Sentence-like messages
|
|
726
|
+
/\s(at least|one|letter|uppercase|lowercase|numeric)/i
|
|
727
|
+
];
|
|
728
|
+
|
|
729
|
+
return validationPatterns.some(pattern => pattern.test(value)) ||
|
|
730
|
+
/message|error|validation|description/i.test(context);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
isFileNameOrDescription(value, context) {
|
|
734
|
+
// Skip file names and descriptions
|
|
735
|
+
const filePatterns = [
|
|
736
|
+
/\.(csv|json|xml|txt|md)$/i,
|
|
737
|
+
/^[a-z_\-]+\.(csv|json|xml|txt)$/i,
|
|
738
|
+
/description|comment|note|foreign key|identity/i
|
|
739
|
+
];
|
|
740
|
+
|
|
741
|
+
return filePatterns.some(pattern => pattern.test(value)) ||
|
|
742
|
+
/description|comment|note|identity|foreign|table/i.test(context);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
isEnvironmentDependentUrl(value) {
|
|
746
|
+
// Only flag URLs that are likely to differ between environments
|
|
747
|
+
const envDependentPatterns = [
|
|
748
|
+
/\.amazonaws\.com/, // AWS services
|
|
749
|
+
/\.azure\.com/, // Azure services
|
|
750
|
+
/\.googleapis\.com/, // Google services
|
|
751
|
+
/api\./, // API endpoints
|
|
752
|
+
/\.dev|\.staging|\.prod/i // Environment-specific domains
|
|
753
|
+
];
|
|
754
|
+
|
|
755
|
+
return envDependentPatterns.some(pattern => pattern.test(value));
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
isRealCredential(value, context) {
|
|
759
|
+
// Check for real credentials, not validation messages
|
|
760
|
+
const credentialKeywords = this.configPatterns.credentials.keywords;
|
|
761
|
+
const lowerContext = context.toLowerCase();
|
|
762
|
+
|
|
763
|
+
// Must have credential keyword in context
|
|
764
|
+
if (!credentialKeywords.some(keyword => lowerContext.includes(keyword))) {
|
|
765
|
+
return false;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// Skip if it's excluded (validation messages, etc.)
|
|
769
|
+
if (this.configPatterns.credentials.exclude.some(pattern => pattern.test(value))) {
|
|
770
|
+
return false;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// Skip validation messages and descriptions
|
|
774
|
+
if (this.isValidationMessage(value, context)) {
|
|
775
|
+
return false;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// Must be reasonably long and not look like UI text
|
|
779
|
+
return value.length >= 6 && !this.looksLikeUIValue(value, context);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
isEnvironmentDependentProperty(propertyName) {
|
|
783
|
+
// Skip UI/framework related property names
|
|
784
|
+
const uiPropertyPatterns = [
|
|
785
|
+
/^key[A-Z]/, // keyXxx (UI field keys)
|
|
786
|
+
/^field[A-Z]/, // fieldXxx
|
|
787
|
+
/^prop[A-Z]/, // propXxx
|
|
788
|
+
/^data[A-Z]/, // dataXxx
|
|
789
|
+
/CheckDisplay/, // UI display control keys
|
|
790
|
+
/InputPossible/, // UI input control keys
|
|
791
|
+
/Flag$/, // UI flags
|
|
792
|
+
/Class$/, // CSS classes
|
|
793
|
+
/^(disabled|readonly|active)Class$/i // UI state classes
|
|
794
|
+
];
|
|
795
|
+
|
|
796
|
+
if (uiPropertyPatterns.some(pattern => pattern.test(propertyName))) {
|
|
797
|
+
return false;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// Properties that are likely to differ between environments
|
|
801
|
+
const envDependentProps = [
|
|
802
|
+
'baseurl', 'baseURL', 'host', 'hostname', 'server',
|
|
803
|
+
'apikey', 'api_key', 'secret', 'token', 'password', 'credential',
|
|
804
|
+
'database', 'db', 'connection', 'connectionstring',
|
|
805
|
+
'timeout', // Only long timeouts
|
|
806
|
+
'port', // Only non-standard ports
|
|
807
|
+
'authorization', 'auth', 'authentication', // Auth headers and codes
|
|
808
|
+
'apptoken', 'devicetoken', 'accesstoken', 'refreshtoken', // App tokens
|
|
809
|
+
'code', 'hash', 'signature', 'key', // Various security values
|
|
810
|
+
'clientsecret', 'clientid', 'sessionkey', // OAuth and session
|
|
811
|
+
'requestid', 'sessionid', 'transactionid', 'otp' // Request/session tracking
|
|
812
|
+
];
|
|
813
|
+
|
|
814
|
+
const lowerName = propertyName.toLowerCase();
|
|
815
|
+
return envDependentProps.some(prop => lowerName.includes(prop));
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
looksLikeEnvironmentConfig(propertyName, value) {
|
|
819
|
+
// Check if this property/value combination looks like environment config
|
|
820
|
+
const lowerPropertyName = propertyName.toLowerCase();
|
|
821
|
+
|
|
822
|
+
if (typeof value === 'string') {
|
|
823
|
+
// Skip test data (common test passwords, etc.)
|
|
824
|
+
const testDataPatterns = [
|
|
825
|
+
/^(password123|test123|admin123|user123|wrongpassword|testpassword)$/i,
|
|
826
|
+
/^(test|mock|dummy|sample|example)/i,
|
|
827
|
+
/^\/(api|mock|test)/, // Test API paths
|
|
828
|
+
/^[a-z]+\d+$/i // Simple test values like 'user1', 'test2'
|
|
829
|
+
];
|
|
830
|
+
|
|
831
|
+
// Don't skip common test patterns if they appear in credential contexts
|
|
832
|
+
const isCredentialContext = /token|key|secret|auth|otp|code|password|credential/i.test(propertyName);
|
|
833
|
+
|
|
834
|
+
if (!isCredentialContext && testDataPatterns.some(pattern => pattern.test(value))) {
|
|
835
|
+
return false;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// Skip object property paths and field names
|
|
839
|
+
const propertyPathPatterns = [
|
|
840
|
+
/^[a-zA-Z][a-zA-Z0-9]*(\[[0-9]+\])?\.[a-zA-Z][a-zA-Z0-9]*$/, // obj[0].prop, obj.prop
|
|
841
|
+
/^[a-zA-Z][a-zA-Z0-9]*\.[a-zA-Z][a-zA-Z0-9]*(\.[a-zA-Z][a-zA-Z0-9]*)*$/, // obj.prop.subprop
|
|
842
|
+
/^[a-zA-Z][a-zA-Z0-9]*(\[[0-9]+\])+$/, // obj[0], obj[0][1]
|
|
843
|
+
/^(key|field|prop|data)[A-Z]/, // keyXxx, fieldXxx, propXxx, dataXxx
|
|
844
|
+
/CheckDisplay|InputPossible|Flag$/i, // Common UI field patterns
|
|
845
|
+
/^exflg|^flg|Support$/i, // Business logic flags
|
|
846
|
+
];
|
|
847
|
+
|
|
848
|
+
if (propertyPathPatterns.some(pattern => pattern.test(value))) {
|
|
849
|
+
return false;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// Skip CSS classes and UI constants
|
|
853
|
+
const uiPatterns = [
|
|
854
|
+
/^bg-|text-|cursor-|border-|flex-|grid-/, // CSS classes
|
|
855
|
+
/^(disabled|readonly|active|inactive)$/i, // UI states
|
|
856
|
+
/class$/i // className values
|
|
857
|
+
];
|
|
858
|
+
|
|
859
|
+
if (uiPatterns.some(pattern => pattern.test(value))) {
|
|
860
|
+
return false;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// Skip internal system identifiers (queue names, service names, route names)
|
|
864
|
+
const systemIdentifierPatterns = [
|
|
865
|
+
/-queue$/i, // Queue names
|
|
866
|
+
/-task$/i, // Task names
|
|
867
|
+
/-activity$/i, // Activity names
|
|
868
|
+
/-service$/i, // Service names
|
|
869
|
+
/-worker$/i, // Worker names
|
|
870
|
+
/^[A-Z_]+_QUEUE$/, // CONSTANT_QUEUE names
|
|
871
|
+
/^[A-Z_]+_TASK$/, // CONSTANT_TASK names
|
|
872
|
+
/^(register|login|logout|reset-password|verify|update)$/i, // Route names
|
|
873
|
+
/password|token/i && /invalid|expired|attempts|exceeded/i // Error messages
|
|
874
|
+
];
|
|
875
|
+
|
|
876
|
+
if (systemIdentifierPatterns.some(pattern => pattern.test(value))) {
|
|
877
|
+
return false;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// Skip error messages and validation messages
|
|
881
|
+
const messagePatterns = [
|
|
882
|
+
/invalid|expired|exceeded|failed|error|success/i,
|
|
883
|
+
/attempts|required|missing|not found/i,
|
|
884
|
+
/^[A-Z][a-z\s]{10,}$/, // Sentence-like messages
|
|
885
|
+
/は|が|を|に|で|と/, // Japanese particles (UI text)
|
|
886
|
+
/情報|画面|ボタン|入力/ // Japanese UI terms
|
|
887
|
+
];
|
|
888
|
+
|
|
889
|
+
if (messagePatterns.some(pattern => pattern.test(value))) {
|
|
890
|
+
return false;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// URLs are environment-dependent
|
|
894
|
+
if (this.configPatterns.urls.regex.test(value)) {
|
|
895
|
+
return this.isEnvironmentDependentUrl(value);
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// Credentials - but exclude test data
|
|
899
|
+
if (lowerPropertyName.includes('key') || lowerPropertyName.includes('secret') ||
|
|
900
|
+
lowerPropertyName.includes('token') || lowerPropertyName.includes('password')) {
|
|
901
|
+
return value.length > 10; // Real secrets are usually longer
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// Skip short endpoint names or simple strings
|
|
905
|
+
if (value.length < 10 && !value.includes('.') && !value.includes('/')) {
|
|
906
|
+
return false;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
if (typeof value === 'number') {
|
|
911
|
+
// Only flag environment-dependent numbers
|
|
912
|
+
return this.configPatterns.environmentNumbers.isEnvironmentDependent(value, propertyName);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
return true;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
isCommonConstant(value) {
|
|
919
|
+
// Common constants that are usually OK to hardcode
|
|
920
|
+
const commonConstants = [100, 200, 300, 400, 500, 1000, 2000, 3000, 5000, 8080, 3000];
|
|
921
|
+
return commonConstants.includes(value);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
isConfigProperty(propertyName) {
|
|
925
|
+
const configProps = [
|
|
926
|
+
'url', 'endpoint', 'baseurl', 'apiurl', 'host', 'port',
|
|
927
|
+
'timeout', 'interval', 'delay', 'retry', 'retries',
|
|
928
|
+
'username', 'password', 'apikey', 'secret', 'token',
|
|
929
|
+
'database', 'connection', 'connectionstring',
|
|
930
|
+
'maxsize', 'batchsize', 'pagesize', 'limit'
|
|
931
|
+
];
|
|
932
|
+
|
|
933
|
+
const lowerName = propertyName.toLowerCase();
|
|
934
|
+
return configProps.some(prop => lowerName.includes(prop));
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
isConfigVariable(variableName) {
|
|
938
|
+
const configVars = [
|
|
939
|
+
'api', 'url', 'endpoint', 'host', 'port',
|
|
940
|
+
'timeout', 'interval', 'delay', 'retry',
|
|
941
|
+
'config', 'setting', 'constant'
|
|
942
|
+
];
|
|
943
|
+
|
|
944
|
+
const lowerName = variableName.toLowerCase();
|
|
945
|
+
return configVars.some(var_ => lowerName.includes(var_));
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
looksLikeHardcodedConfig(name, value) {
|
|
949
|
+
// Skip obvious constants and UI values
|
|
950
|
+
if (typeof value === 'string') {
|
|
951
|
+
if (value.length < 3) return false;
|
|
952
|
+
if (/^(ok|yes|no|true|false|success|error|info|warn)$/i.test(value)) return false;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
if (typeof value === 'number') {
|
|
956
|
+
if (this.isCommonConstant(value)) return false;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
return true;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
createMessage(config) {
|
|
963
|
+
const baseMessage = 'Environment-dependent configuration should not be hardcoded.';
|
|
964
|
+
|
|
965
|
+
switch (config.type) {
|
|
966
|
+
case 'url':
|
|
967
|
+
return `${baseMessage} External URL '${config.value}' should be loaded from environment variables or configuration files.`;
|
|
968
|
+
case 'credential':
|
|
969
|
+
return `${baseMessage} Credential value '${config.value}' should be loaded from secure environment variables.`;
|
|
970
|
+
case 'environment_config':
|
|
971
|
+
return `${baseMessage} Environment-dependent value ${config.value} should be configurable via environment variables or config files.`;
|
|
972
|
+
case 'connection':
|
|
973
|
+
return `${baseMessage} Connection string should be loaded from environment variables.`;
|
|
974
|
+
case 'property_config':
|
|
975
|
+
return `${baseMessage} Property '${config.propertyName}' may contain environment-dependent value '${config.value}'.`;
|
|
976
|
+
case 'variable_config':
|
|
977
|
+
return `${baseMessage} Variable '${config.variableName}' may contain environment-dependent value '${config.value}'.`;
|
|
978
|
+
case 'config_key':
|
|
979
|
+
return `${baseMessage} Configuration key '${config.value}' should not be hardcoded.`;
|
|
980
|
+
default:
|
|
981
|
+
return `${baseMessage} Value '${config.value}' may differ between environments.`;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
getSuggestion(type) {
|
|
986
|
+
const suggestions = {
|
|
987
|
+
'url': 'Use process.env.API_URL or config.get("api.url")',
|
|
988
|
+
'credential': 'Use process.env.SECRET_KEY or secure vault',
|
|
989
|
+
'environment_config': 'Move to environment variables or config service',
|
|
990
|
+
'connection': 'Use process.env.DATABASE_URL',
|
|
991
|
+
'property_config': 'Consider if this differs between dev/staging/production',
|
|
992
|
+
'variable_config': 'Use environment variables if this differs between environments',
|
|
993
|
+
'config_key': 'Use constants or enums for configuration keys'
|
|
994
|
+
};
|
|
995
|
+
|
|
996
|
+
return suggestions[type] || 'Consider if this value should differ between dev/staging/production environments';
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
module.exports = C067SymbolBasedAnalyzer;
|