@sun-asterisk/sunlint 1.3.16 → 1.3.17
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 +3 -3
- package/config/rules/enhanced-rules-registry.json +40 -20
- package/core/cli-action-handler.js +2 -2
- package/core/config-merger.js +28 -6
- package/core/constants/defaults.js +1 -1
- package/core/file-targeting-service.js +72 -4
- package/core/output-service.js +21 -4
- package/engines/heuristic-engine.js +5 -0
- package/package.json +1 -1
- package/rules/common/C002_no_duplicate_code/README.md +115 -0
- package/rules/common/C002_no_duplicate_code/analyzer.js +615 -219
- package/rules/common/C002_no_duplicate_code/test-cases/api-handlers.ts +64 -0
- package/rules/common/C002_no_duplicate_code/test-cases/data-processor.ts +46 -0
- package/rules/common/C002_no_duplicate_code/test-cases/good-example.tsx +40 -0
- package/rules/common/C002_no_duplicate_code/test-cases/product-service.ts +57 -0
- package/rules/common/C002_no_duplicate_code/test-cases/user-service.ts +49 -0
- package/rules/common/C008/analyzer.js +40 -0
- package/rules/common/C008/config.json +20 -0
- package/rules/common/C008/ts-morph-analyzer.js +1067 -0
- package/rules/common/C018_no_throw_generic_error/analyzer.js +1 -1
- package/rules/common/C018_no_throw_generic_error/symbol-based-analyzer.js +27 -3
- package/rules/common/C024_no_scatter_hardcoded_constants/symbol-based-analyzer.js +504 -162
- package/rules/common/C029_catch_block_logging/analyzer.js +499 -89
- package/rules/common/C033_separate_service_repository/README.md +131 -20
- package/rules/common/C033_separate_service_repository/analyzer.js +1 -1
- package/rules/common/C033_separate_service_repository/symbol-based-analyzer.js +417 -274
- package/rules/common/C041_no_sensitive_hardcode/analyzer.js +144 -254
- package/rules/common/C041_no_sensitive_hardcode/config.json +50 -0
- package/rules/common/C041_no_sensitive_hardcode/symbol-based-analyzer.js +575 -0
- package/rules/common/C067_no_hardcoded_config/analyzer.js +17 -16
- package/rules/common/C067_no_hardcoded_config/symbol-based-analyzer.js +3477 -659
- package/rules/docs/C002_no_duplicate_code.md +276 -11
- package/rules/index.js +5 -1
- package/rules/security/S006_no_plaintext_recovery_codes/analyzer.js +266 -88
- package/rules/security/S006_no_plaintext_recovery_codes/symbol-based-analyzer.js +805 -0
- package/rules/security/S010_no_insecure_encryption/README.md +78 -0
- package/rules/security/S010_no_insecure_encryption/analyzer.js +463 -398
- package/rules/security/S013_tls_enforcement/README.md +51 -0
- package/rules/security/S013_tls_enforcement/analyzer.js +99 -0
- package/rules/security/S013_tls_enforcement/config.json +41 -0
- package/rules/security/S013_tls_enforcement/symbol-based-analyzer.js +339 -0
- package/rules/security/S014_tls_version_enforcement/README.md +354 -0
- package/rules/security/S014_tls_version_enforcement/analyzer.js +118 -0
- package/rules/security/S014_tls_version_enforcement/config.json +56 -0
- package/rules/security/S014_tls_version_enforcement/symbol-based-analyzer.js +194 -0
- package/rules/security/S055_content_type_validation/analyzer.js +121 -279
- package/rules/security/S055_content_type_validation/symbol-based-analyzer.js +346 -0
- package/rules/tests/C002_no_duplicate_code.test.js +111 -22
- package/rules/common/C029_catch_block_logging/analyzer-smart-pipeline.js +0 -755
- package/rules/common/C041_no_sensitive_hardcode/ast-analyzer.js +0 -296
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* C041 Symbol-based Analyzer - Do not hardcode or push sensitive information
|
|
3
|
+
* Purpose: Prevent hardcoded secrets, tokens, API keys, passwords, and sensitive URLs in code
|
|
4
|
+
*/
|
|
5
|
+
const { SyntaxKind } = require('ts-morph');
|
|
6
|
+
|
|
7
|
+
class C041SymbolBasedAnalyzer {
|
|
8
|
+
constructor(semanticEngine = null) {
|
|
9
|
+
this.ruleId = 'C041';
|
|
10
|
+
this.ruleName = 'Do not hardcode or push sensitive information (token, API key, secret, URL)';
|
|
11
|
+
this.semanticEngine = semanticEngine;
|
|
12
|
+
this.verbose = false;
|
|
13
|
+
|
|
14
|
+
// === Sensitive Patterns ===
|
|
15
|
+
this.sensitivePatterns = {
|
|
16
|
+
// API Keys and Tokens
|
|
17
|
+
apiKey: /\b(api[_-]?key|apikey|api[_-]?token)\s*[:=]\s*["'`]([a-zA-Z0-9_\-]{20,})["'`]/gi,
|
|
18
|
+
|
|
19
|
+
// AWS credentials
|
|
20
|
+
awsAccessKey: /\b(aws[_-]?access[_-]?key[_-]?id|aws[_-]?secret[_-]?access[_-]?key)\s*[:=]\s*["'`]([A-Z0-9]{20,})["'`]/gi,
|
|
21
|
+
|
|
22
|
+
// Private keys
|
|
23
|
+
privateKey: /(-----BEGIN\s+(RSA\s+)?PRIVATE\s+KEY-----)/gi,
|
|
24
|
+
|
|
25
|
+
// Database connection strings
|
|
26
|
+
dbConnection: /(mongodb|mysql|postgresql|postgres):\/\/[^:]+:[^@]+@/gi,
|
|
27
|
+
|
|
28
|
+
// JWT secrets
|
|
29
|
+
jwtSecret: /\b(jwt[_-]?secret|secret[_-]?key)\s*[:=]\s*["'`]([a-zA-Z0-9_\-]{16,})["'`]/gi,
|
|
30
|
+
|
|
31
|
+
// Bearer tokens
|
|
32
|
+
bearerToken: /\b(bearer|authorization)\s*[:=]\s*["'`]([a-zA-Z0-9_\-\.]{20,})["'`]/gi,
|
|
33
|
+
|
|
34
|
+
// OAuth tokens
|
|
35
|
+
oauthToken: /\b(oauth[_-]?token|access[_-]?token|refresh[_-]?token)\s*[:=]\s*["'`]([a-zA-Z0-9_\-\.]{20,})["'`]/gi,
|
|
36
|
+
|
|
37
|
+
// Generic secrets
|
|
38
|
+
secret: /\b(secret|password|passwd|pwd)\s*[:=]\s*["'`](?!.*(\$\{|process\.env|env\.|config\.))([^"'`\s]{8,})["'`]/gi,
|
|
39
|
+
|
|
40
|
+
// API endpoints with embedded credentials
|
|
41
|
+
credentialUrl: /(https?:\/\/[^:\/]+:[^@\/]+@[^\s"'`]+)/gi,
|
|
42
|
+
|
|
43
|
+
// SSH keys
|
|
44
|
+
sshKey: /(ssh-rsa\s+[A-Za-z0-9+\/=]{100,})/gi,
|
|
45
|
+
|
|
46
|
+
// Google API keys
|
|
47
|
+
googleApiKey: /AIza[0-9A-Za-z\-_]{35}/gi,
|
|
48
|
+
|
|
49
|
+
// Slack tokens
|
|
50
|
+
slackToken: /xox[baprs]-[0-9]{10,13}-[0-9]{10,13}-[a-zA-Z0-9]{24,}/gi,
|
|
51
|
+
|
|
52
|
+
// GitHub tokens
|
|
53
|
+
githubToken: /gh[pousr]_[A-Za-z0-9_]{36,}/gi,
|
|
54
|
+
|
|
55
|
+
// Stripe keys
|
|
56
|
+
stripeKey: /sk_live_[0-9a-zA-Z]{24,}/gi,
|
|
57
|
+
|
|
58
|
+
// Generic high-entropy strings (potential secrets) - DISABLED, too many false positives
|
|
59
|
+
// highEntropy: /["'`]([a-zA-Z0-9+\/=_\-]{32,})["'`]/g
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// === Variable name patterns that suggest sensitive data ===
|
|
63
|
+
this.sensitiveVariableNames = [
|
|
64
|
+
/api[_-]?key/i,
|
|
65
|
+
/secret/i,
|
|
66
|
+
/password/i,
|
|
67
|
+
/passwd/i,
|
|
68
|
+
/pass/i,
|
|
69
|
+
/token/i,
|
|
70
|
+
/auth/i,
|
|
71
|
+
/credential/i,
|
|
72
|
+
/private[_-]?key/i,
|
|
73
|
+
/access[_-]?key/i,
|
|
74
|
+
/client[_-]?secret/i,
|
|
75
|
+
/encryption[_-]?key/i
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
// === Safe patterns to exclude (env vars, config imports) ===
|
|
79
|
+
this.safePatterns = [
|
|
80
|
+
/process\.env\./,
|
|
81
|
+
/import\.meta\.env\./,
|
|
82
|
+
/env\./,
|
|
83
|
+
/config\./,
|
|
84
|
+
/Config\./,
|
|
85
|
+
/\$\{.*\}/, // Template literals with variables
|
|
86
|
+
/getEnv\(/,
|
|
87
|
+
/loadConfig\(/,
|
|
88
|
+
/readSecret\(/,
|
|
89
|
+
/vault\./i,
|
|
90
|
+
/secretManager\./i,
|
|
91
|
+
/keyVault\./i
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
// === Ignore Configuration ===
|
|
95
|
+
this.ignoredFilePatterns = [
|
|
96
|
+
/test\//,
|
|
97
|
+
/tests\//,
|
|
98
|
+
/__tests__\//,
|
|
99
|
+
/\.test\./,
|
|
100
|
+
/\.spec\./,
|
|
101
|
+
/node_modules\//,
|
|
102
|
+
/\.env\.example$/,
|
|
103
|
+
/\.env\.template$/,
|
|
104
|
+
/mock/i,
|
|
105
|
+
/sample/i,
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
// Common placeholder/dummy values to ignore
|
|
109
|
+
this.ignoredValues = [
|
|
110
|
+
'your-api-key-here',
|
|
111
|
+
'your_api_key',
|
|
112
|
+
'mock',
|
|
113
|
+
'xxx',
|
|
114
|
+
'yyy',
|
|
115
|
+
'zzz',
|
|
116
|
+
'example',
|
|
117
|
+
'test',
|
|
118
|
+
'dummy',
|
|
119
|
+
'placeholder',
|
|
120
|
+
'changeme',
|
|
121
|
+
'replace-me'
|
|
122
|
+
];
|
|
123
|
+
|
|
124
|
+
// Patterns for non-sensitive constant values (event names, actions, etc.)
|
|
125
|
+
this.nonSensitivePatterns = [
|
|
126
|
+
/^(use|on|handle)[A-Z]/, // React hooks: usePassword, onPasswordChange
|
|
127
|
+
/_trigger$/i, // Event triggers: password_trigger
|
|
128
|
+
/_event$/i, // Event names: password_event
|
|
129
|
+
/_action$/i, // Action names: change_password_action
|
|
130
|
+
/_validation$/i, // Validation keys: password_validation
|
|
131
|
+
/_field$/i, // Form fields: password_field
|
|
132
|
+
/_input$/i, // Input names: password_input
|
|
133
|
+
/_activity$/i, // Activity names: updatePassword-activity
|
|
134
|
+
/_service$/i, // Service names: auth-service
|
|
135
|
+
/_handler$/i, // Handler names: password-handler
|
|
136
|
+
/_method$/i, // Method names: resetPassword-method
|
|
137
|
+
/^(change|update|set|get|renew|reset|forgot)[A-Z_]/i, // Action verbs: changePassword, renewpassword
|
|
138
|
+
/^[a-z_]+_(type|mode|status|state)$/i, // State keys: password_type, auth_mode
|
|
139
|
+
/^\/[a-zA-Z0-9_\/-]+$/i, // URL-like paths: /forgot_password, /reset_password
|
|
140
|
+
/^[a-z][a-zA-Z0-9]*-[a-z]+$/, // Kebab-case identifiers: updatePassword-activity
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
// Variable/constant names that contain error messages or config (not secrets)
|
|
144
|
+
this.errorMessageConstants = [
|
|
145
|
+
/^[A-Z_]+ERROR[A-Z_]*$/, // COGNITO_ERROR, API_ERROR
|
|
146
|
+
/^[A-Z_]+MESSAGE[A-Z_]*$/, // MESSAGE_LOG, ERROR_MESSAGE
|
|
147
|
+
/^[A-Z_]+EXCEPTION[A-Z_]*$/, // AUTH_EXCEPTION
|
|
148
|
+
/^[A-Z_]+CODE[A-Z_]*$/, // ERROR_CODE, STATUS_CODE
|
|
149
|
+
/^[A-Z_]+STATUS[A-Z_]*$/, // USER_STATUS
|
|
150
|
+
/^[A-Z_]+TYPE[A-Z_]*$/, // MESSAGE_TYPE
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
// Parent object names that contain service/activity mappings (not secrets)
|
|
154
|
+
this.serviceConfigConstants = [
|
|
155
|
+
/^activity$/i,
|
|
156
|
+
/^activities$/i,
|
|
157
|
+
/^service$/i,
|
|
158
|
+
/^services$/i,
|
|
159
|
+
/^handler$/i,
|
|
160
|
+
/^handlers$/i,
|
|
161
|
+
/^method$/i,
|
|
162
|
+
/^methods$/i,
|
|
163
|
+
/^action$/i,
|
|
164
|
+
/^actions$/i,
|
|
165
|
+
/^route$/i,
|
|
166
|
+
/^routes$/i,
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
// Patterns in values that indicate error messages (not secrets)
|
|
170
|
+
this.errorMessagePatterns = [
|
|
171
|
+
/Exception$/, // NotAuthorizedException
|
|
172
|
+
/Error$/, // ValidationError
|
|
173
|
+
/\s+(not\s+found|invalid|expired|exceeded|failed)/i, // "User not found", "Token expired"
|
|
174
|
+
/^[A-Z][a-z]+([A-Z][a-z]+)*Exception$/, // PascalCaseException
|
|
175
|
+
/^[A-Z][a-z]+\s/, // Sentence case messages: "Email not found"
|
|
176
|
+
];
|
|
177
|
+
|
|
178
|
+
// Patterns that indicate legitimate long strings (not secrets)
|
|
179
|
+
this.legitimateStringPatterns = [
|
|
180
|
+
/\sas\s+["'`]/i, // SQL aliases: AS "columnName"
|
|
181
|
+
/SELECT\s/i, // SQL queries
|
|
182
|
+
/FROM\s/i, // SQL queries
|
|
183
|
+
/WHERE\s/i, // SQL queries
|
|
184
|
+
/JOIN\s/i, // SQL queries
|
|
185
|
+
/\.[a-z_]+\s+as\s+/i, // SQL column aliases
|
|
186
|
+
/^[a-z_]+\.[a-z_]+/i, // Dot notation: table.column
|
|
187
|
+
/\s+AS\s+/, // SQL AS keyword
|
|
188
|
+
/<[^>]+>/, // HTML/XML tags
|
|
189
|
+
/^(SELECT|INSERT|UPDATE|DELETE|CREATE|ALTER|DROP)/i, // SQL statements
|
|
190
|
+
/^\/[^\/]+\//, // Regex patterns or paths
|
|
191
|
+
/^\w+:\/\//, // URLs (but not with credentials)
|
|
192
|
+
];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async initialize(semanticEngine = null) {
|
|
196
|
+
if (semanticEngine) {
|
|
197
|
+
this.semanticEngine = semanticEngine;
|
|
198
|
+
}
|
|
199
|
+
this.verbose = semanticEngine?.verbose || false;
|
|
200
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
201
|
+
console.log(`🔧 [C041 Symbol-Based] Analyzer initialized, verbose: ${this.verbose}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async analyzeFileBasic(filePath, options = {}) {
|
|
206
|
+
return await this.analyzeFileWithSymbols(filePath, options);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async analyzeFileWithSymbols(filePath, options = {}) {
|
|
210
|
+
const violations = [];
|
|
211
|
+
const verbose = options.verbose || this.verbose;
|
|
212
|
+
|
|
213
|
+
if (!this.semanticEngine?.project) {
|
|
214
|
+
if (verbose) {
|
|
215
|
+
console.warn('[C041 Symbol-Based] No semantic engine available, skipping analysis');
|
|
216
|
+
}
|
|
217
|
+
return violations;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (this.shouldIgnoreFile(filePath)) {
|
|
221
|
+
if (verbose) console.log(`[${this.ruleId}] Ignoring ${filePath}`);
|
|
222
|
+
return violations;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (verbose) {
|
|
226
|
+
console.log(`🔍 [C041 Symbol-Based] Starting analysis for ${filePath}`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
const sourceFile = this.semanticEngine.project.getSourceFile(filePath);
|
|
231
|
+
if (!sourceFile) {
|
|
232
|
+
if (verbose) {
|
|
233
|
+
console.log(`⚠️ [C041] Could not load source file: ${filePath}`);
|
|
234
|
+
}
|
|
235
|
+
return violations;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Check string literals
|
|
239
|
+
this.checkStringLiterals(sourceFile, violations);
|
|
240
|
+
|
|
241
|
+
// Check variable declarations
|
|
242
|
+
this.checkVariableDeclarations(sourceFile, violations);
|
|
243
|
+
|
|
244
|
+
// Check property assignments
|
|
245
|
+
this.checkPropertyAssignments(sourceFile, violations);
|
|
246
|
+
|
|
247
|
+
// Check template literals
|
|
248
|
+
this.checkTemplateLiterals(sourceFile, violations);
|
|
249
|
+
|
|
250
|
+
if (verbose) {
|
|
251
|
+
console.log(`✅ [C041] Found ${violations.length} violations in ${filePath}`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
} catch (error) {
|
|
255
|
+
if (verbose) {
|
|
256
|
+
console.warn(`[C041 Symbol-Based] Analysis failed for ${filePath}:`, error.message);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return violations;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
checkStringLiterals(sourceFile, violations) {
|
|
264
|
+
const stringLiterals = sourceFile.getDescendantsOfKind(SyntaxKind.StringLiteral);
|
|
265
|
+
|
|
266
|
+
stringLiterals.forEach(literal => {
|
|
267
|
+
const text = literal.getText();
|
|
268
|
+
const value = literal.getLiteralValue();
|
|
269
|
+
|
|
270
|
+
// Skip if it's a legitimate long string (SQL, HTML, etc.)
|
|
271
|
+
if (this.isLegitimateString(value)) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Skip if it's a safe pattern (env variable reference)
|
|
276
|
+
if (this.isSafeContext(literal)) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Check against sensitive patterns
|
|
281
|
+
this.checkAgainstPatterns(literal, value, sourceFile, violations);
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
checkVariableDeclarations(sourceFile, violations) {
|
|
286
|
+
const variableDeclarations = sourceFile.getDescendantsOfKind(SyntaxKind.VariableDeclaration);
|
|
287
|
+
|
|
288
|
+
variableDeclarations.forEach(varDecl => {
|
|
289
|
+
const name = varDecl.getName();
|
|
290
|
+
const initializer = varDecl.getInitializer();
|
|
291
|
+
|
|
292
|
+
if (!initializer) return;
|
|
293
|
+
|
|
294
|
+
// Check if variable name suggests sensitive data
|
|
295
|
+
const isSensitiveName = this.sensitiveVariableNames.some(pattern => pattern.test(name));
|
|
296
|
+
|
|
297
|
+
if (isSensitiveName) {
|
|
298
|
+
const initText = initializer.getText();
|
|
299
|
+
|
|
300
|
+
// Skip if using env variables or config
|
|
301
|
+
if (this.isSafeValue(initText)) {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Check if it's a hardcoded value
|
|
306
|
+
if (SyntaxKind.StringLiteral === initializer.getKind() ||
|
|
307
|
+
SyntaxKind.NoSubstitutionTemplateLiteral === initializer.getKind()) {
|
|
308
|
+
const value = initializer.getText().replace(/^["'`]|["'`]$/g, '');
|
|
309
|
+
|
|
310
|
+
// Skip if the value itself is a non-sensitive constant (event names, actions, etc.)
|
|
311
|
+
if (this.isNonSensitiveConstant(value)) {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// More lenient check: any non-trivial string in a sensitive variable is suspicious
|
|
316
|
+
if (value.length >= 8 && !this.isIgnoredValue(value)) {
|
|
317
|
+
violations.push(this.createViolation(
|
|
318
|
+
varDecl,
|
|
319
|
+
sourceFile,
|
|
320
|
+
`Variable '${name}' appears to contain hardcoded sensitive information`
|
|
321
|
+
));
|
|
322
|
+
}
|
|
323
|
+
} else if (SyntaxKind.TemplateExpression === initializer.getKind()) {
|
|
324
|
+
// Check template literals for hardcoded secrets in the string parts
|
|
325
|
+
const templateText = initializer.getText();
|
|
326
|
+
|
|
327
|
+
// Check if template has hardcoded secrets (not just variables)
|
|
328
|
+
if (!this.isSafeValue(templateText) && this.hasHardcodedSecretInTemplate(templateText)) {
|
|
329
|
+
violations.push(this.createViolation(
|
|
330
|
+
varDecl,
|
|
331
|
+
sourceFile,
|
|
332
|
+
`Variable '${name}' template literal contains hardcoded sensitive information`
|
|
333
|
+
));
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
checkPropertyAssignments(sourceFile, violations) {
|
|
341
|
+
const propertyAssignments = sourceFile.getDescendantsOfKind(SyntaxKind.PropertyAssignment);
|
|
342
|
+
|
|
343
|
+
propertyAssignments.forEach(prop => {
|
|
344
|
+
const name = prop.getName();
|
|
345
|
+
const initializer = prop.getInitializer();
|
|
346
|
+
|
|
347
|
+
if (!initializer) return;
|
|
348
|
+
|
|
349
|
+
// Skip if parent is an error/message constant object
|
|
350
|
+
const parent = prop.getParent()?.getParent();
|
|
351
|
+
if (parent && parent.getKind() === SyntaxKind.VariableDeclaration) {
|
|
352
|
+
const parentName = parent.asKind(SyntaxKind.VariableDeclaration)?.getName();
|
|
353
|
+
if (parentName && (this.isErrorMessageConstant(parentName) || this.isServiceConfigConstant(parentName))) {
|
|
354
|
+
return; // Skip properties inside error/service constant objects
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Also check grandparent for nested objects like activity.account
|
|
359
|
+
const grandParent = prop.getParent()?.getParent()?.getParent()?.getParent();
|
|
360
|
+
if (grandParent && grandParent.getKind() === SyntaxKind.VariableDeclaration) {
|
|
361
|
+
const grandParentName = grandParent.asKind(SyntaxKind.VariableDeclaration)?.getName();
|
|
362
|
+
if (grandParentName && (this.isErrorMessageConstant(grandParentName) || this.isServiceConfigConstant(grandParentName))) {
|
|
363
|
+
return; // Skip properties inside nested service config objects
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const isSensitiveName = this.sensitiveVariableNames.some(pattern => pattern.test(name));
|
|
368
|
+
|
|
369
|
+
if (isSensitiveName) {
|
|
370
|
+
const initText = initializer.getText();
|
|
371
|
+
|
|
372
|
+
if (this.isSafeValue(initText)) {
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (SyntaxKind.StringLiteral === initializer.getKind() ||
|
|
377
|
+
SyntaxKind.NoSubstitutionTemplateLiteral === initializer.getKind()) {
|
|
378
|
+
const value = initializer.getText().replace(/^["'`]|["'`]$/g, '');
|
|
379
|
+
|
|
380
|
+
// Skip if the value itself is a non-sensitive constant
|
|
381
|
+
if (this.isNonSensitiveConstant(value)) {
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Skip if the value looks like an error message
|
|
386
|
+
if (this.isErrorMessage(value)) {
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// More lenient check for properties with sensitive names
|
|
391
|
+
if (value.length >= 8 && !this.isIgnoredValue(value)) {
|
|
392
|
+
violations.push(this.createViolation(
|
|
393
|
+
prop,
|
|
394
|
+
sourceFile,
|
|
395
|
+
`Property '${name}' appears to contain hardcoded sensitive information`
|
|
396
|
+
));
|
|
397
|
+
}
|
|
398
|
+
} else if (SyntaxKind.TemplateExpression === initializer.getKind()) {
|
|
399
|
+
const templateText = initializer.getText();
|
|
400
|
+
|
|
401
|
+
if (!this.isSafeValue(templateText) && this.hasHardcodedSecretInTemplate(templateText)) {
|
|
402
|
+
violations.push(this.createViolation(
|
|
403
|
+
prop,
|
|
404
|
+
sourceFile,
|
|
405
|
+
`Property '${name}' template literal contains hardcoded sensitive information`
|
|
406
|
+
));
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
checkTemplateLiterals(sourceFile, violations) {
|
|
414
|
+
const templates = sourceFile.getDescendantsOfKind(SyntaxKind.TemplateExpression);
|
|
415
|
+
|
|
416
|
+
templates.forEach(template => {
|
|
417
|
+
const text = template.getText();
|
|
418
|
+
|
|
419
|
+
// Check if template contains hardcoded secrets even with variables
|
|
420
|
+
if (this.hasHardcodedSecretInTemplate(text)) {
|
|
421
|
+
// Check against patterns
|
|
422
|
+
this.checkAgainstPatterns(template, text, sourceFile, violations);
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
checkAgainstPatterns(node, value, sourceFile, violations) {
|
|
428
|
+
for (const [patternName, pattern] of Object.entries(this.sensitivePatterns)) {
|
|
429
|
+
pattern.lastIndex = 0; // Reset regex
|
|
430
|
+
const matches = pattern.exec(value);
|
|
431
|
+
|
|
432
|
+
if (matches) {
|
|
433
|
+
const matchedValue = matches[2] || matches[1] || matches[0];
|
|
434
|
+
|
|
435
|
+
if (this.isIgnoredValue(matchedValue)) {
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
violations.push(this.createViolation(
|
|
440
|
+
node,
|
|
441
|
+
sourceFile,
|
|
442
|
+
`Potential hardcoded sensitive data detected (${patternName})`
|
|
443
|
+
));
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
isSafeContext(node) {
|
|
449
|
+
let parent = node.getParent();
|
|
450
|
+
let depth = 0;
|
|
451
|
+
const maxDepth = 5;
|
|
452
|
+
|
|
453
|
+
while (parent && depth < maxDepth) {
|
|
454
|
+
const text = parent.getText();
|
|
455
|
+
|
|
456
|
+
if (this.safePatterns.some(pattern => pattern.test(text))) {
|
|
457
|
+
return true;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
parent = parent.getParent();
|
|
461
|
+
depth++;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return false;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
isSafeValue(value) {
|
|
468
|
+
return this.safePatterns.some(pattern => pattern.test(value));
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
isLikelySecret(value) {
|
|
472
|
+
// Check length
|
|
473
|
+
if (value.length < 8) return false;
|
|
474
|
+
|
|
475
|
+
// Check if it looks like a placeholder
|
|
476
|
+
if (this.isIgnoredValue(value.toLowerCase())) return false;
|
|
477
|
+
|
|
478
|
+
// Check entropy (randomness)
|
|
479
|
+
const entropy = this.calculateEntropy(value);
|
|
480
|
+
return entropy > 3.5; // High entropy suggests random/generated secret
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
calculateEntropy(str) {
|
|
484
|
+
const len = str.length;
|
|
485
|
+
const frequencies = {};
|
|
486
|
+
|
|
487
|
+
for (let i = 0; i < len; i++) {
|
|
488
|
+
const char = str[i];
|
|
489
|
+
frequencies[char] = (frequencies[char] || 0) + 1;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
let entropy = 0;
|
|
493
|
+
for (const freq of Object.values(frequencies)) {
|
|
494
|
+
const p = freq / len;
|
|
495
|
+
entropy -= p * Math.log2(p);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
return entropy;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
isIgnoredValue(value) {
|
|
502
|
+
const lowerValue = value.toLowerCase();
|
|
503
|
+
return this.ignoredValues.some(ignored =>
|
|
504
|
+
lowerValue.includes(ignored.toLowerCase())
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
isNonSensitiveConstant(nameOrValue) {
|
|
509
|
+
return this.nonSensitivePatterns.some(pattern => pattern.test(nameOrValue));
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
isLegitimateString(value) {
|
|
513
|
+
// Check if the string matches legitimate patterns (SQL, HTML, etc.)
|
|
514
|
+
return this.legitimateStringPatterns.some(pattern => pattern.test(value));
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
isErrorMessageConstant(name) {
|
|
518
|
+
return this.errorMessageConstants.some(pattern => pattern.test(name));
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
isErrorMessage(value) {
|
|
522
|
+
return this.errorMessagePatterns.some(pattern => pattern.test(value));
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
isServiceConfigConstant(name) {
|
|
526
|
+
return this.serviceConfigConstants.some(pattern => pattern.test(name));
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
hasHardcodedSecretInTemplate(templateText) {
|
|
530
|
+
// Extract the static string parts (not the ${...} parts)
|
|
531
|
+
const staticParts = templateText.split(/\$\{[^}]+\}/);
|
|
532
|
+
|
|
533
|
+
// Check if any static part contains what looks like a secret
|
|
534
|
+
for (const part of staticParts) {
|
|
535
|
+
// Check for patterns like "secret=xxx", "token=xxx", "key=xxx"
|
|
536
|
+
if (/(?:secret|token|key|password|passwd)[:=][^&\s`]{8,}/i.test(part)) {
|
|
537
|
+
return true;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Check for high entropy strings in URL parameters
|
|
541
|
+
if (/[:=]([a-zA-Z0-9_\-]{16,})/.test(part)) {
|
|
542
|
+
const match = part.match(/[:=]([a-zA-Z0-9_\-]{16,})/);
|
|
543
|
+
if (match && match[1] && !this.isIgnoredValue(match[1])) {
|
|
544
|
+
const entropy = this.calculateEntropy(match[1]);
|
|
545
|
+
if (entropy > 3.5) {
|
|
546
|
+
return true;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
return false;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
createViolation(node, sourceFile, message) {
|
|
556
|
+
return {
|
|
557
|
+
ruleId: this.ruleId,
|
|
558
|
+
severity: 'warning',
|
|
559
|
+
message: message,
|
|
560
|
+
source: this.ruleId,
|
|
561
|
+
file: sourceFile.getFilePath(),
|
|
562
|
+
line: node.getStartLineNumber(),
|
|
563
|
+
column: node.getStart() - node.getStartLinePos(),
|
|
564
|
+
description: `[SYMBOL-BASED] ${message}`,
|
|
565
|
+
suggestion: 'Remove hardcoded sensitive information and use secure storage or environment variables instead.',
|
|
566
|
+
category: 'security',
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
shouldIgnoreFile(filePath) {
|
|
571
|
+
return this.ignoredFilePatterns.some((pattern) => pattern.test(filePath));
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
module.exports = C041SymbolBasedAnalyzer;
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
// rules/common/C067_no_hardcoded_config/analyzer.js
|
|
2
|
-
const C067SymbolBasedAnalyzer = require(
|
|
2
|
+
const C067SymbolBasedAnalyzer = require("./symbol-based-analyzer.js");
|
|
3
3
|
|
|
4
4
|
class C067Analyzer {
|
|
5
5
|
constructor(semanticEngine = null) {
|
|
6
|
-
this.ruleId =
|
|
7
|
-
this.ruleName =
|
|
8
|
-
this.description =
|
|
6
|
+
this.ruleId = "C067";
|
|
7
|
+
this.ruleName = "Do not hardcode configuration inside code";
|
|
8
|
+
this.description =
|
|
9
|
+
"Improve configurability, reduce risk when changing environments, and make configuration management flexible and maintainable. Avoid hardcoding API URLs, credentials, timeouts, retry intervals, batch sizes, and feature flags.";
|
|
9
10
|
this.semanticEngine = semanticEngine;
|
|
10
11
|
this.verbose = false;
|
|
11
|
-
|
|
12
|
-
// Use symbol-based
|
|
12
|
+
|
|
13
|
+
// Use symbol-based analyzer with ts-morph for accurate AST analysis
|
|
13
14
|
this.symbolAnalyzer = new C067SymbolBasedAnalyzer(semanticEngine);
|
|
14
15
|
}
|
|
15
16
|
|
|
@@ -24,12 +25,12 @@ class C067Analyzer {
|
|
|
24
25
|
// Main analyze method required by heuristic engine
|
|
25
26
|
async analyze(files, language, options = {}) {
|
|
26
27
|
const violations = [];
|
|
27
|
-
|
|
28
|
+
|
|
28
29
|
for (const filePath of files) {
|
|
29
30
|
if (options.verbose) {
|
|
30
|
-
console.log(`[DEBUG] 🎯 C067: Analyzing ${filePath.split(
|
|
31
|
+
console.log(`[DEBUG] 🎯 C067: Analyzing ${filePath.split("/").pop()}`);
|
|
31
32
|
}
|
|
32
|
-
|
|
33
|
+
|
|
33
34
|
try {
|
|
34
35
|
const fileViolations = await this.analyzeFileBasic(filePath, options);
|
|
35
36
|
violations.push(...fileViolations);
|
|
@@ -37,7 +38,7 @@ class C067Analyzer {
|
|
|
37
38
|
console.warn(`C067: Skipping ${filePath}: ${error.message}`);
|
|
38
39
|
}
|
|
39
40
|
}
|
|
40
|
-
|
|
41
|
+
|
|
41
42
|
return violations;
|
|
42
43
|
}
|
|
43
44
|
|
|
@@ -46,11 +47,11 @@ class C067Analyzer {
|
|
|
46
47
|
// Try semantic engine first, fallback to standalone ts-morph
|
|
47
48
|
if (this.semanticEngine?.isSymbolEngineReady?.() && this.semanticEngine.project) {
|
|
48
49
|
if (this.verbose) {
|
|
49
|
-
console.log(`[DEBUG] 🎯 C067: Using semantic engine for ${filePath.split(
|
|
50
|
+
console.log(`[DEBUG] 🎯 C067: Using semantic engine for ${filePath.split("/").pop()}`);
|
|
50
51
|
}
|
|
51
|
-
|
|
52
|
+
|
|
52
53
|
const violations = await this.symbolAnalyzer.analyzeFileBasic(filePath, options);
|
|
53
|
-
|
|
54
|
+
|
|
54
55
|
if (this.verbose) {
|
|
55
56
|
console.log(`[DEBUG] 🎯 C067: Symbol-based analysis found ${violations.length} violations`);
|
|
56
57
|
}
|
|
@@ -59,11 +60,11 @@ class C067Analyzer {
|
|
|
59
60
|
} else {
|
|
60
61
|
// Fallback to standalone analysis
|
|
61
62
|
if (this.verbose) {
|
|
62
|
-
console.log(`[DEBUG] 🎯 C067: Using standalone analysis for ${filePath.split(
|
|
63
|
+
console.log(`[DEBUG] 🎯 C067: Using standalone analysis for ${filePath.split("/").pop()}`);
|
|
63
64
|
}
|
|
64
|
-
|
|
65
|
+
|
|
65
66
|
const violations = await this.symbolAnalyzer.analyzeFileStandalone(filePath, options);
|
|
66
|
-
|
|
67
|
+
|
|
67
68
|
if (this.verbose) {
|
|
68
69
|
console.log(`[DEBUG] 🎯 C067: Standalone analysis found ${violations.length} violations`);
|
|
69
70
|
}
|