@sun-asterisk/sunlint 1.3.26 → 1.3.28
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/rules/enhanced-rules-registry.json +101 -17
- package/config/rules/rules-registry-generated.json +22 -22
- package/origin-rules/security-en.md +351 -338
- package/package.json +1 -1
- package/rules/common/C003_no_vague_abbreviations/analyzer.js +73 -21
- package/rules/common/C017_constructor_logic/symbol-based-analyzer.js +206 -2
- package/rules/common/C024_no_scatter_hardcoded_constants/symbol-based-analyzer.js +553 -58
- package/rules/common/C029_catch_block_logging/analyzer.js +47 -12
- package/rules/common/C033_separate_service_repository/symbol-based-analyzer.js +35 -15
- package/rules/common/C041_no_sensitive_hardcode/symbol-based-analyzer.js +9 -5
- package/rules/security/S003_open_redirect_protection/README.md +371 -0
- package/rules/security/S003_open_redirect_protection/analyzer.js +135 -0
- package/rules/security/S003_open_redirect_protection/config.json +58 -0
- package/rules/security/S003_open_redirect_protection/symbol-based-analyzer.js +884 -0
- package/rules/security/S004_sensitive_data_logging/analyzer.js +135 -0
- package/rules/security/S004_sensitive_data_logging/config.json +62 -0
- package/rules/security/S004_sensitive_data_logging/symbol-based-analyzer.js +592 -0
- package/rules/security/S005_no_origin_auth/analyzer.js +97 -148
- package/rules/security/S005_no_origin_auth/config.json +28 -67
- package/rules/security/S005_no_origin_auth/symbol-based-analyzer.js +708 -0
- package/rules/security/S006_no_plaintext_recovery_codes/symbol-based-analyzer.js +170 -31
- package/rules/security/S010_no_insecure_encryption/analyzer.js +8 -2
- package/rules/security/S012_hardcoded_secrets/analyzer.js +149 -0
- package/rules/security/S012_hardcoded_secrets/config.json +75 -0
- package/rules/security/S012_hardcoded_secrets/symbol-based-analyzer.js +1204 -0
- package/rules/security/S013_tls_enforcement/symbol-based-analyzer.js +87 -0
- package/rules/security/S017_use_parameterized_queries/analyzer.js +11 -78
- package/rules/security/S017_use_parameterized_queries/symbol-based-analyzer.js +1146 -1
- package/rules/security/S019_smtp_injection_protection/analyzer.js +120 -0
- package/rules/security/S019_smtp_injection_protection/config.json +35 -0
- package/rules/security/S019_smtp_injection_protection/symbol-based-analyzer.js +687 -0
- package/rules/security/S020_no_eval_dynamic_code/analyzer.js +55 -130
- package/rules/security/S020_no_eval_dynamic_code/symbol-based-analyzer.js +4 -19
- package/rules/security/S022_escape_output_context/README.md +254 -0
- package/rules/security/S022_escape_output_context/analyzer.js +510 -0
- package/rules/security/S022_escape_output_context/config.json +229 -0
- package/rules/security/S023_no_json_injection/analyzer.js +15 -0
- package/rules/security/S023_no_json_injection/ast-analyzer.js +18 -3
- package/rules/security/S023_no_json_injection/config.json +133 -0
- package/rules/security/S024_xpath_xxe_protection/regex-based-analyzer.js +41 -0
- package/rules/security/S027_no_hardcoded_secrets/analyzer.js +67 -8
- package/rules/security/S027_no_hardcoded_secrets/categorized-analyzer.js +29 -6
- package/rules/security/S029_csrf_protection/config.json +127 -0
- package/rules/security/S030_directory_browsing_protection/regex-based-analyzer.js +160 -28
- package/rules/security/S030_directory_browsing_protection/symbol-based-analyzer.js +81 -19
- package/rules/security/S031_secure_session_cookies/analyzer.js +20 -2
- package/rules/security/S031_secure_session_cookies/regex-based-analyzer.js +100 -0
- package/rules/security/S031_secure_session_cookies/symbol-based-analyzer.js +8 -1
- package/rules/security/S032_httponly_session_cookies/analyzer.js +2 -2
- package/rules/security/S032_httponly_session_cookies/regex-based-analyzer.js +115 -0
- package/rules/security/S032_httponly_session_cookies/symbol-based-analyzer.js +39 -10
- package/rules/security/S036_lfi_rfi_protection/analyzer.js +224 -0
- package/rules/security/S036_lfi_rfi_protection/config.json +20 -0
- package/rules/security/S040_session_fixation_protection/analyzer.js +153 -0
- package/rules/security/S040_session_fixation_protection/config.json +20 -0
- package/rules/security/S042_require_re_authentication_for_long_lived/README.md +83 -0
- package/rules/security/S042_require_re_authentication_for_long_lived/analyzer.js +153 -0
- package/rules/security/S042_require_re_authentication_for_long_lived/config.json +41 -0
- package/rules/security/S042_require_re_authentication_for_long_lived/symbol-based-analyzer.js +1139 -0
- package/rules/security/S043_password_changes_invalidate_all_sessions/README.md +107 -0
- package/rules/security/S043_password_changes_invalidate_all_sessions/analyzer.js +153 -0
- package/rules/security/S043_password_changes_invalidate_all_sessions/config.json +41 -0
- package/rules/security/S043_password_changes_invalidate_all_sessions/symbol-based-analyzer.js +541 -0
- package/docs/COMMAND-EXAMPLES.md +0 -390
- package/docs/FILE_LIMITS_COMPLETION_REPORT.md +0 -151
- package/docs/FOLDER_STRUCTURE.md +0 -59
- package/docs/SIMPLIFIED_USAGE_GUIDE.md +0 -208
- package/rules/security/S017_use_parameterized_queries/regex-based-analyzer.js +0 -541
- package/rules/security/S020_no_eval_dynamic_code/regex-based-analyzer.js +0 -307
|
@@ -0,0 +1,1204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule S012: Hardcoded Secrets Detection
|
|
3
|
+
*
|
|
4
|
+
* Detects hardcoded secrets, API keys, passwords, tokens, and other sensitive
|
|
5
|
+
* credentials in source code. This rule helps prevent accidental exposure of
|
|
6
|
+
* secrets through version control and reduces security risks.
|
|
7
|
+
*
|
|
8
|
+
* OWASP: A02:2021 - Cryptographic Failures
|
|
9
|
+
* CWE: CWE-798 - Use of Hard-coded Credentials
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const { SyntaxKind } = require("ts-morph");
|
|
13
|
+
|
|
14
|
+
class S012SymbolBasedAnalyzer {
|
|
15
|
+
constructor(semanticEngine = null) {
|
|
16
|
+
this.ruleId = "S012";
|
|
17
|
+
this.semanticEngine = semanticEngine;
|
|
18
|
+
|
|
19
|
+
// Patterns for secret variable names
|
|
20
|
+
this.secretVariablePatterns = [
|
|
21
|
+
'password',
|
|
22
|
+
'passwd',
|
|
23
|
+
'pwd',
|
|
24
|
+
'secret',
|
|
25
|
+
'api_key',
|
|
26
|
+
'apikey',
|
|
27
|
+
'api-key',
|
|
28
|
+
'access_key',
|
|
29
|
+
'accesskey',
|
|
30
|
+
'secret_key',
|
|
31
|
+
'secretkey',
|
|
32
|
+
'private_key',
|
|
33
|
+
'privatekey',
|
|
34
|
+
'token',
|
|
35
|
+
'auth_token',
|
|
36
|
+
'authtoken',
|
|
37
|
+
'access_token',
|
|
38
|
+
'accesstoken',
|
|
39
|
+
'refresh_token',
|
|
40
|
+
'refreshtoken',
|
|
41
|
+
'jwt_secret',
|
|
42
|
+
'jwtsecret',
|
|
43
|
+
'encryption_key',
|
|
44
|
+
'encryptionkey',
|
|
45
|
+
'database_password',
|
|
46
|
+
'db_password',
|
|
47
|
+
'db_pass',
|
|
48
|
+
'connection_string',
|
|
49
|
+
'connectionstring',
|
|
50
|
+
'credentials',
|
|
51
|
+
'auth_token',
|
|
52
|
+
'auth_key',
|
|
53
|
+
'auth_secret',
|
|
54
|
+
'bearer_token',
|
|
55
|
+
'bearer',
|
|
56
|
+
'certificate',
|
|
57
|
+
'ssh_key',
|
|
58
|
+
'sshkey',
|
|
59
|
+
'oauth_secret',
|
|
60
|
+
'oauthsecret',
|
|
61
|
+
'client_secret',
|
|
62
|
+
'clientsecret',
|
|
63
|
+
'master_key',
|
|
64
|
+
'masterkey',
|
|
65
|
+
'admin_password',
|
|
66
|
+
'root_password',
|
|
67
|
+
'app_secret',
|
|
68
|
+
'appsecret',
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
// Patterns for secret values (high entropy strings)
|
|
72
|
+
this.secretValuePatterns = [
|
|
73
|
+
// AWS Access Keys
|
|
74
|
+
/AKIA[0-9A-Z]{16}/,
|
|
75
|
+
// AWS Secret Keys (40 characters base64)
|
|
76
|
+
/aws(.{0,20})?['\"][0-9a-zA-Z\/+]{40}['\"]/i,
|
|
77
|
+
// Generic API Keys (32+ chars with mixed case/numbers)
|
|
78
|
+
/['\"][a-z0-9_-]{32,}['\"]/i,
|
|
79
|
+
// JWT tokens
|
|
80
|
+
/eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]*/,
|
|
81
|
+
// Generic secrets (high entropy, 20+ chars)
|
|
82
|
+
/['\"][a-zA-Z0-9+/=_-]{20,}['\"]/,
|
|
83
|
+
// GitHub tokens
|
|
84
|
+
/gh[pousr]_[A-Za-z0-9_]{36,}/,
|
|
85
|
+
// Slack tokens
|
|
86
|
+
/xox[baprs]-[0-9]{10,13}-[0-9]{10,13}-[a-zA-Z0-9]{24,}/,
|
|
87
|
+
// Google API keys
|
|
88
|
+
/AIza[0-9A-Za-z_-]{35}/,
|
|
89
|
+
// Firebase URLs with secrets
|
|
90
|
+
/[a-z0-9-]+\.firebaseio\.com/,
|
|
91
|
+
// Private keys
|
|
92
|
+
/-----BEGIN (RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/,
|
|
93
|
+
// Connection strings with passwords
|
|
94
|
+
/(mongodb|postgres|mysql):\/\/[^:]+:[^@]+@/i,
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
// Safe patterns to exclude (environment variables, function calls, etc.)
|
|
98
|
+
this.safePatterns = [
|
|
99
|
+
'process.env',
|
|
100
|
+
'env.',
|
|
101
|
+
'getenv',
|
|
102
|
+
'config.get',
|
|
103
|
+
'configservice.get', // NestJS ConfigService
|
|
104
|
+
'.get(', // Any .get() method call
|
|
105
|
+
'.getstring(',
|
|
106
|
+
'.getnumber(',
|
|
107
|
+
'vault.get',
|
|
108
|
+
'secrets.get',
|
|
109
|
+
'secretsmanager',
|
|
110
|
+
'keyvault',
|
|
111
|
+
'parameter.store',
|
|
112
|
+
'ssm.get',
|
|
113
|
+
'dotenv',
|
|
114
|
+
'.env',
|
|
115
|
+
'import.meta.env',
|
|
116
|
+
'vite_',
|
|
117
|
+
'next_public_',
|
|
118
|
+
'react_app_',
|
|
119
|
+
'vue_app_',
|
|
120
|
+
'example',
|
|
121
|
+
'test',
|
|
122
|
+
'mock',
|
|
123
|
+
'dummy',
|
|
124
|
+
'fake',
|
|
125
|
+
'placeholder',
|
|
126
|
+
'your_',
|
|
127
|
+
'your-',
|
|
128
|
+
'<your',
|
|
129
|
+
'xxx',
|
|
130
|
+
'***',
|
|
131
|
+
'...',
|
|
132
|
+
'todo',
|
|
133
|
+
'fixme',
|
|
134
|
+
'changeme',
|
|
135
|
+
'replace',
|
|
136
|
+
'insert',
|
|
137
|
+
'.split(', // token.split() is not a secret
|
|
138
|
+
'.slice(', // string slicing
|
|
139
|
+
'.substring(', // string operations
|
|
140
|
+
];
|
|
141
|
+
|
|
142
|
+
// Common test/example values that are safe
|
|
143
|
+
this.safeTestValues = [
|
|
144
|
+
'test',
|
|
145
|
+
'testing',
|
|
146
|
+
'example',
|
|
147
|
+
'demo',
|
|
148
|
+
'sample',
|
|
149
|
+
'mock',
|
|
150
|
+
'dummy',
|
|
151
|
+
'fake',
|
|
152
|
+
'localhost',
|
|
153
|
+
'127.0.0.1',
|
|
154
|
+
'0.0.0.0',
|
|
155
|
+
'admin',
|
|
156
|
+
'12345',
|
|
157
|
+
'password',
|
|
158
|
+
'secret',
|
|
159
|
+
'changeme',
|
|
160
|
+
'your-secret-here',
|
|
161
|
+
'your-key-here',
|
|
162
|
+
'xxx',
|
|
163
|
+
'***',
|
|
164
|
+
];
|
|
165
|
+
|
|
166
|
+
// Minimum entropy threshold for considering a string as secret
|
|
167
|
+
this.minEntropy = 3.5;
|
|
168
|
+
|
|
169
|
+
// Minimum length for secret strings
|
|
170
|
+
this.minSecretLength = 16;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Main analysis method
|
|
175
|
+
*/
|
|
176
|
+
async analyze(sourceFile, filePath = "") {
|
|
177
|
+
const violations = [];
|
|
178
|
+
|
|
179
|
+
// Skip test files and config files that commonly have example secrets
|
|
180
|
+
if (this.shouldSkipFile(filePath)) {
|
|
181
|
+
return violations;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Check variable declarations
|
|
185
|
+
const variableDeclarations = sourceFile.getDescendantsOfKind(SyntaxKind.VariableDeclaration);
|
|
186
|
+
for (const varDecl of variableDeclarations) {
|
|
187
|
+
const violation = this.checkVariableDeclaration(varDecl, filePath);
|
|
188
|
+
if (violation) {
|
|
189
|
+
violations.push(violation);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Check property assignments
|
|
194
|
+
const propertyAssignments = sourceFile.getDescendantsOfKind(SyntaxKind.PropertyAssignment);
|
|
195
|
+
for (const propAssign of propertyAssignments) {
|
|
196
|
+
const violation = this.checkPropertyAssignment(propAssign, filePath);
|
|
197
|
+
if (violation) {
|
|
198
|
+
violations.push(violation);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Check string literals for hardcoded secrets
|
|
203
|
+
const stringLiterals = sourceFile.getDescendantsOfKind(SyntaxKind.StringLiteral);
|
|
204
|
+
for (const strLit of stringLiterals) {
|
|
205
|
+
const violation = this.checkStringLiteral(strLit, filePath);
|
|
206
|
+
if (violation) {
|
|
207
|
+
violations.push(violation);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return violations;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Check if file should be skipped
|
|
216
|
+
*/
|
|
217
|
+
shouldSkipFile(filePath) {
|
|
218
|
+
const skipPatterns = [
|
|
219
|
+
/test\//i,
|
|
220
|
+
/tests\//i,
|
|
221
|
+
/__tests__\//i,
|
|
222
|
+
/\.test\./i,
|
|
223
|
+
/\.spec\./i,
|
|
224
|
+
/node_modules\//i,
|
|
225
|
+
/\.example\./i,
|
|
226
|
+
/\.sample\./i,
|
|
227
|
+
/\.template\./i,
|
|
228
|
+
/dist\//i,
|
|
229
|
+
/build\//i,
|
|
230
|
+
/coverage\//i,
|
|
231
|
+
];
|
|
232
|
+
|
|
233
|
+
return skipPatterns.some(pattern => pattern.test(filePath));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Check variable declarations for hardcoded secrets
|
|
238
|
+
*/
|
|
239
|
+
checkVariableDeclaration(varDecl, filePath) {
|
|
240
|
+
const name = varDecl.getName();
|
|
241
|
+
const initializer = varDecl.getInitializer();
|
|
242
|
+
|
|
243
|
+
if (!initializer) {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const nameLower = name.toLowerCase();
|
|
248
|
+
|
|
249
|
+
// Skip error constant names
|
|
250
|
+
if (this.isErrorConstant(name)) {
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Skip destructuring patterns like { token } or { password }
|
|
255
|
+
if (this.isDestructuringPattern(name)) {
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Skip documentation/schema variables (Swagger, Docs, Schema, Dto)
|
|
260
|
+
if (this.isDocumentationVariable(name)) {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Skip counter/attempt variables (failedPasswordAttempts, loginAttempts)
|
|
265
|
+
if (this.isCounterVariable(name)) {
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Skip error collection variables (tokenErrors, passwordErrors)
|
|
270
|
+
if (this.isErrorCollectionVariable(name)) {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Skip boolean check variables (isValidToken, hasPassword)
|
|
275
|
+
if (this.isBooleanCheckVariable(name)) {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Skip utility function/class names (generatePassword, IsPassword)
|
|
280
|
+
if (this.isUtilityFunctionOrClass(name)) {
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Skip framework-specific internal variables (__next_navigation_guard_token)
|
|
285
|
+
if (this.isFrameworkInternalVariable(name)) {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Skip error constant properties (tokenExpired, invalidToken)
|
|
290
|
+
if (this.isErrorConstantProperty(name)) {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Skip React/Vue state variables and selectors
|
|
295
|
+
if (this.isStateVariableOrSelector(name)) {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Skip format/pattern/regex definitions
|
|
300
|
+
if (this.isFormatOrPatternDefinition(name)) {
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Check if variable name suggests it's a secret
|
|
305
|
+
const isSuspiciousName = this.secretVariablePatterns.some(pattern =>
|
|
306
|
+
nameLower.includes(pattern)
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
if (!isSuspiciousName) {
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Check if it's using a safe pattern (env variable, config, etc.)
|
|
314
|
+
const initText = initializer.getText();
|
|
315
|
+
if (this.isSafePattern(initText)) {
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Skip if it's a URL/endpoint variable (apiGetTokenUrl = '/api/token')
|
|
320
|
+
if (this.isURLOrEndpointVariable(name, initText)) {
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Skip if extracting from query parameters (get(searchParams, 'token'))
|
|
325
|
+
if (this.isQueryParameterExtraction(initText)) {
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Skip if accessing framework internal property (nextState?.__next_navigation_guard_token)
|
|
330
|
+
if (this.isAccessingFrameworkInternal(initText)) {
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Skip if initializer is a property access (e.g., AWS.config.credentials)
|
|
335
|
+
if (this.isPropertyAccess(initText)) {
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Skip if initializer is a constructor call (e.g., new SecretClient(...))
|
|
340
|
+
if (this.isConstructorCall(initText)) {
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Skip if initializer is an await expression for secret retrieval
|
|
345
|
+
if (this.isAsyncSecretRetrieval(initText)) {
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Skip if initializer is a function/method call
|
|
350
|
+
if (this.isFunctionOrMethodCall(initText)) {
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Skip if extracting from API response (response.data['...'].AccessToken)
|
|
355
|
+
if (this.isExtractingFromAPIResponse(initText)) {
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Skip if it's an object literal with only references (no hardcoded values)
|
|
360
|
+
if (this.isObjectLiteralWithOnlyReferences(initializer)) {
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// For UPPERCASE constants, be more strict - only flag if value is actually hardcoded
|
|
365
|
+
// This allows PASSWORD_REGEX = /.../ but flags API_KEY = "hardcoded-key"
|
|
366
|
+
const isConstant = this.isConstantName(name);
|
|
367
|
+
|
|
368
|
+
// Check if the value looks like a hardcoded secret
|
|
369
|
+
if (this.isHardcodedSecret(initText, nameLower, isConstant)) {
|
|
370
|
+
return {
|
|
371
|
+
line: varDecl.getStartLineNumber(),
|
|
372
|
+
column: varDecl.getStart() - varDecl.getStartLinePos(),
|
|
373
|
+
message: `Hardcoded secret detected: Variable '${name}' appears to contain a hardcoded secret. Use environment variables or a secret management system instead.`,
|
|
374
|
+
severity: "error",
|
|
375
|
+
ruleId: this.ruleId,
|
|
376
|
+
secretType: this.identifySecretType(nameLower),
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Check property assignments for hardcoded secrets
|
|
385
|
+
*/
|
|
386
|
+
checkPropertyAssignment(propAssign, filePath) {
|
|
387
|
+
const name = propAssign.getName();
|
|
388
|
+
const initializer = propAssign.getInitializer();
|
|
389
|
+
|
|
390
|
+
if (!initializer) {
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const nameLower = name.toLowerCase();
|
|
395
|
+
|
|
396
|
+
// Skip error constant property names
|
|
397
|
+
if (this.isErrorConstant(name)) {
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Skip function-like property names (verbs + nouns)
|
|
402
|
+
if (this.isFunctionName(name)) {
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Skip documentation/schema properties
|
|
407
|
+
if (this.isDocumentationVariable(name)) {
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Skip counter/attempt properties
|
|
412
|
+
if (this.isCounterVariable(name)) {
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Skip error collection properties
|
|
417
|
+
if (this.isErrorCollectionVariable(name)) {
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Skip boolean check properties
|
|
422
|
+
if (this.isBooleanCheckVariable(name)) {
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Skip utility function/class names
|
|
427
|
+
if (this.isUtilityFunctionOrClass(name)) {
|
|
428
|
+
return null;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Skip framework-specific internal variables
|
|
432
|
+
if (this.isFrameworkInternalVariable(name)) {
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Skip error constant properties (tokenExpired, invalidToken)
|
|
437
|
+
if (this.isErrorConstantProperty(name)) {
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Skip if this is part of an error object structure
|
|
442
|
+
if (this.isPartOfErrorObject(propAssign)) {
|
|
443
|
+
return null;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Skip if this is just a property name (not a hardcoded value)
|
|
447
|
+
if (this.isPropertyNameOnly(propAssign)) {
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Skip React/Vue state variables and selectors
|
|
452
|
+
if (this.isStateVariableOrSelector(name)) {
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Skip format/pattern/regex definitions
|
|
457
|
+
if (this.isFormatOrPatternDefinition(name)) {
|
|
458
|
+
return null;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Check if property name suggests it's a secret
|
|
462
|
+
const isSuspiciousName = this.secretVariablePatterns.some(pattern =>
|
|
463
|
+
nameLower.includes(pattern)
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
if (!isSuspiciousName) {
|
|
467
|
+
return null;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const initText = initializer.getText();
|
|
471
|
+
if (this.isSafePattern(initText)) {
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Skip if initializer is a function/method call
|
|
476
|
+
if (this.isFunctionOrMethodCall(initText)) {
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Skip if it's a simple string literal in constant definitions (message strings)
|
|
481
|
+
if (this.isMessageString(initText)) {
|
|
482
|
+
return null;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (this.isHardcodedSecret(initText, nameLower)) {
|
|
486
|
+
return {
|
|
487
|
+
line: propAssign.getStartLineNumber(),
|
|
488
|
+
column: propAssign.getStart() - propAssign.getStartLinePos(),
|
|
489
|
+
message: `Hardcoded secret detected: Property '${name}' appears to contain a hardcoded secret. Use environment variables or a secret management system instead.`,
|
|
490
|
+
severity: "error",
|
|
491
|
+
ruleId: this.ruleId,
|
|
492
|
+
secretType: this.identifySecretType(nameLower),
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
return null;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Check string literals for patterns that look like secrets
|
|
501
|
+
*/
|
|
502
|
+
checkStringLiteral(strLit, filePath) {
|
|
503
|
+
const text = strLit.getText();
|
|
504
|
+
const literalValue = strLit.getLiteralValue();
|
|
505
|
+
|
|
506
|
+
// Skip short strings
|
|
507
|
+
if (literalValue.length < this.minSecretLength) {
|
|
508
|
+
return null;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Skip if it's a safe test value
|
|
512
|
+
if (this.isSafeTestValue(literalValue)) {
|
|
513
|
+
return null;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Skip if this is part of SQL query (addSelect, select, query strings)
|
|
517
|
+
if (this.isPartOfSQLQuery(strLit)) {
|
|
518
|
+
return null;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Check if parent is using safe patterns
|
|
522
|
+
const parent = strLit.getParent();
|
|
523
|
+
if (parent) {
|
|
524
|
+
const parentText = parent.getText();
|
|
525
|
+
if (this.isSafePattern(parentText)) {
|
|
526
|
+
return null;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Check for known secret patterns
|
|
531
|
+
for (const pattern of this.secretValuePatterns) {
|
|
532
|
+
if (pattern.test(literalValue)) {
|
|
533
|
+
// Additional validation for high entropy
|
|
534
|
+
if (this.calculateEntropy(literalValue) >= this.minEntropy) {
|
|
535
|
+
return {
|
|
536
|
+
line: strLit.getStartLineNumber(),
|
|
537
|
+
column: strLit.getStart() - strLit.getStartLinePos(),
|
|
538
|
+
message: `Potential hardcoded secret detected: String literal matches known secret pattern. Consider using environment variables or a secret management system.`,
|
|
539
|
+
severity: "error",
|
|
540
|
+
ruleId: this.ruleId,
|
|
541
|
+
secretType: "unknown",
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
return null;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Check if text contains safe patterns
|
|
552
|
+
*/
|
|
553
|
+
isSafePattern(text) {
|
|
554
|
+
const textLower = text.toLowerCase();
|
|
555
|
+
return this.safePatterns.some(pattern => textLower.includes(pattern));
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Check if value is a safe test value
|
|
560
|
+
*/
|
|
561
|
+
isSafeTestValue(value) {
|
|
562
|
+
const valueLower = value.toLowerCase();
|
|
563
|
+
return this.safeTestValues.some(testVal => valueLower === testVal);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Check if a value is likely a hardcoded secret
|
|
568
|
+
*/
|
|
569
|
+
isHardcodedSecret(value, variableName = '', isConstant = false) {
|
|
570
|
+
// Remove quotes if present
|
|
571
|
+
const cleanValue = value.replace(/^['"`]|['"`]$/g, '');
|
|
572
|
+
|
|
573
|
+
// Skip empty or very short values
|
|
574
|
+
if (cleanValue.length < 8) {
|
|
575
|
+
return false;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Skip if it's a safe test value
|
|
579
|
+
if (this.isSafeTestValue(cleanValue)) {
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// For UPPERCASE constants, skip if it's not a string literal
|
|
584
|
+
// This allows: PASSWORD_REGEX = /.../, TOKEN_STATUS = {...}
|
|
585
|
+
// But flags: API_KEY = "hardcoded-secret"
|
|
586
|
+
if (isConstant) {
|
|
587
|
+
// Only flag if it's a quoted string (likely a hardcoded secret)
|
|
588
|
+
if (!value.match(/^['"`]/)) {
|
|
589
|
+
return false;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Check for specific secret patterns
|
|
594
|
+
for (const pattern of this.secretValuePatterns) {
|
|
595
|
+
if (pattern.test(value)) {
|
|
596
|
+
return true;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Check entropy for generic secrets
|
|
601
|
+
if (cleanValue.length >= this.minSecretLength) {
|
|
602
|
+
const entropy = this.calculateEntropy(cleanValue);
|
|
603
|
+
|
|
604
|
+
// Higher entropy threshold if variable name is suspicious
|
|
605
|
+
const isSuspiciousVar = this.secretVariablePatterns.some(p =>
|
|
606
|
+
variableName.includes(p)
|
|
607
|
+
);
|
|
608
|
+
|
|
609
|
+
const threshold = isSuspiciousVar ? 3.0 : this.minEntropy;
|
|
610
|
+
|
|
611
|
+
if (entropy >= threshold) {
|
|
612
|
+
// Additional check: ensure it has mixed characters
|
|
613
|
+
const hasMixedCase = /[a-z]/.test(cleanValue) && /[A-Z]/.test(cleanValue);
|
|
614
|
+
const hasNumbers = /[0-9]/.test(cleanValue);
|
|
615
|
+
const hasSpecialChars = /[^a-zA-Z0-9]/.test(cleanValue);
|
|
616
|
+
|
|
617
|
+
// At least 2 of 3 characteristics for high confidence
|
|
618
|
+
const characteristics = [hasMixedCase, hasNumbers, hasSpecialChars].filter(Boolean).length;
|
|
619
|
+
|
|
620
|
+
if (characteristics >= 2) {
|
|
621
|
+
return true;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
return false;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Calculate Shannon entropy of a string
|
|
631
|
+
*/
|
|
632
|
+
calculateEntropy(str) {
|
|
633
|
+
const len = str.length;
|
|
634
|
+
const frequencies = {};
|
|
635
|
+
|
|
636
|
+
for (let i = 0; i < len; i++) {
|
|
637
|
+
const char = str[i];
|
|
638
|
+
frequencies[char] = (frequencies[char] || 0) + 1;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
let entropy = 0;
|
|
642
|
+
for (const char in frequencies) {
|
|
643
|
+
const probability = frequencies[char] / len;
|
|
644
|
+
entropy -= probability * Math.log2(probability);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
return entropy;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Identify the type of secret based on variable name
|
|
652
|
+
*/
|
|
653
|
+
identifySecretType(name) {
|
|
654
|
+
if (name.includes('password') || name.includes('passwd') || name.includes('pwd')) {
|
|
655
|
+
return 'password';
|
|
656
|
+
}
|
|
657
|
+
if (name.includes('api') && (name.includes('key') || name.includes('token'))) {
|
|
658
|
+
return 'api_key';
|
|
659
|
+
}
|
|
660
|
+
if (name.includes('access') && name.includes('key')) {
|
|
661
|
+
return 'access_key';
|
|
662
|
+
}
|
|
663
|
+
if (name.includes('secret') && name.includes('key')) {
|
|
664
|
+
return 'secret_key';
|
|
665
|
+
}
|
|
666
|
+
if (name.includes('private') && name.includes('key')) {
|
|
667
|
+
return 'private_key';
|
|
668
|
+
}
|
|
669
|
+
if (name.includes('token')) {
|
|
670
|
+
return 'token';
|
|
671
|
+
}
|
|
672
|
+
if (name.includes('jwt')) {
|
|
673
|
+
return 'jwt_secret';
|
|
674
|
+
}
|
|
675
|
+
if (name.includes('connection') && name.includes('string')) {
|
|
676
|
+
return 'connection_string';
|
|
677
|
+
}
|
|
678
|
+
if (name.includes('credentials')) {
|
|
679
|
+
return 'credentials';
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
return 'secret';
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Check if name is a constant (UPPER_CASE style)
|
|
687
|
+
*/
|
|
688
|
+
isConstantName(name) {
|
|
689
|
+
// All uppercase with underscores (e.g., TOKEN_STATUS, PASSWORD_REGEX, DB_PASSWORD)
|
|
690
|
+
return /^[A-Z_][A-Z0-9_]*$/.test(name);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* Check if name is an error constant
|
|
695
|
+
*/
|
|
696
|
+
isErrorConstant(name) {
|
|
697
|
+
// Pattern: *_E_* or FE_E_* or BE_E_* or ERROR_*
|
|
698
|
+
return /_E_/i.test(name) || /^(FE|BE|API)_E_/i.test(name) || /^ERROR_/i.test(name);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* Check if name is a destructuring pattern
|
|
703
|
+
*/
|
|
704
|
+
isDestructuringPattern(name) {
|
|
705
|
+
// Destructuring patterns like { token } will have curly braces
|
|
706
|
+
return name.includes('{') || name.includes('}');
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* Check if name looks like a function name
|
|
711
|
+
*/
|
|
712
|
+
isFunctionName(name) {
|
|
713
|
+
// Common function name patterns (verb + noun)
|
|
714
|
+
const functionVerbs = [
|
|
715
|
+
'get', 'set', 'send', 'receive', 'fetch', 'update', 'delete', 'create',
|
|
716
|
+
'verify', 'validate', 'check', 'is', 'has', 'can', 'should',
|
|
717
|
+
'reset', 'change', 'generate', 'encode', 'decode', 'encrypt', 'decrypt',
|
|
718
|
+
'handle', 'process', 'execute', 'run', 'start', 'stop'
|
|
719
|
+
];
|
|
720
|
+
|
|
721
|
+
const nameLower = name.toLowerCase();
|
|
722
|
+
|
|
723
|
+
// Check if starts with a verb
|
|
724
|
+
return functionVerbs.some(verb => {
|
|
725
|
+
const verbPattern = new RegExp(`^${verb}[A-Z]`);
|
|
726
|
+
return verbPattern.test(name) || nameLower.startsWith(verb + '_');
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* Check if text is a property access (e.g., AWS.config.credentials)
|
|
732
|
+
*/
|
|
733
|
+
isPropertyAccess(text) {
|
|
734
|
+
// Contains dot notation and doesn't start with a quote
|
|
735
|
+
return /^[a-zA-Z_$][a-zA-Z0-9_$]*\.[a-zA-Z0-9_.]+$/.test(text.trim());
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Check if text is a constructor call (e.g., new SecretClient(...))
|
|
740
|
+
*/
|
|
741
|
+
isConstructorCall(text) {
|
|
742
|
+
return /^new\s+[A-Z][a-zA-Z0-9_]*\s*\(/.test(text.trim());
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* Check if text is an async secret retrieval (e.g., await secretClient.getSecret(...))
|
|
747
|
+
*/
|
|
748
|
+
isAsyncSecretRetrieval(text) {
|
|
749
|
+
const textLower = text.toLowerCase();
|
|
750
|
+
return textLower.startsWith('await') &&
|
|
751
|
+
(textLower.includes('getsecret') ||
|
|
752
|
+
textLower.includes('get(') ||
|
|
753
|
+
textLower.includes('fetch'));
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Check if text is a function or method call
|
|
758
|
+
*/
|
|
759
|
+
isFunctionOrMethodCall(text) {
|
|
760
|
+
const trimmed = text.trim();
|
|
761
|
+
// Function call: functionName(...) or obj.method(...) or await func(...)
|
|
762
|
+
return /^(await\s+)?[a-zA-Z_$][a-zA-Z0-9_$.]*\s*\(/.test(trimmed) ||
|
|
763
|
+
// Method chain: obj.method1().method2()
|
|
764
|
+
/\)\s*\.\s*[a-zA-Z_$][a-zA-Z0-9_$]*\s*\(/.test(trimmed);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* Check if text is a message string (descriptive text, not a secret)
|
|
769
|
+
*/
|
|
770
|
+
isMessageString(text) {
|
|
771
|
+
const trimmed = text.trim();
|
|
772
|
+
|
|
773
|
+
// Must be a quoted string
|
|
774
|
+
if (!trimmed.match(/^['"`]/)) {
|
|
775
|
+
return false;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
const content = trimmed.replace(/^['"`]|['"`]$/g, '').toLowerCase();
|
|
779
|
+
|
|
780
|
+
// Message strings typically contain spaces and common words
|
|
781
|
+
const messageIndicators = [
|
|
782
|
+
' is ',
|
|
783
|
+
' are ',
|
|
784
|
+
' has ',
|
|
785
|
+
' have ',
|
|
786
|
+
' invalid',
|
|
787
|
+
' expired',
|
|
788
|
+
' not found',
|
|
789
|
+
' failed',
|
|
790
|
+
' error',
|
|
791
|
+
' exceeded',
|
|
792
|
+
' attempts',
|
|
793
|
+
' required',
|
|
794
|
+
' missing',
|
|
795
|
+
];
|
|
796
|
+
|
|
797
|
+
return messageIndicators.some(indicator => content.includes(indicator));
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* Check if name is a documentation/schema variable
|
|
802
|
+
*/
|
|
803
|
+
isDocumentationVariable(name) {
|
|
804
|
+
const docSuffixes = ['Swagger', 'Docs', 'Doc', 'Schema', 'Dto', 'Documentation', 'Spec'];
|
|
805
|
+
return docSuffixes.some(suffix => name.endsWith(suffix));
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* Check if name is a counter/attempt variable
|
|
810
|
+
*/
|
|
811
|
+
isCounterVariable(name) {
|
|
812
|
+
const counterPatterns = [
|
|
813
|
+
/attempts?$/i, // loginAttempts, failedAttempts
|
|
814
|
+
/count$/i, // passwordCount, tokenCount
|
|
815
|
+
/counter$/i, // loginCounter
|
|
816
|
+
/limit$/i, // passwordLimit
|
|
817
|
+
/max.*attempts?/i, // maxFailedAttempts
|
|
818
|
+
/failed.*attempts?/i, // failedPasswordAttempts
|
|
819
|
+
];
|
|
820
|
+
return counterPatterns.some(pattern => pattern.test(name));
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* Check if name is an error collection variable
|
|
825
|
+
*/
|
|
826
|
+
isErrorCollectionVariable(name) {
|
|
827
|
+
const nameLower = name.toLowerCase();
|
|
828
|
+
return nameLower.endsWith('errors') || // tokenErrors, passwordErrors
|
|
829
|
+
nameLower.endsWith('error') || // tokenError
|
|
830
|
+
nameLower.includes('errormessage') || // errorMessages
|
|
831
|
+
nameLower.includes('errorlist'); // errorList
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
/**
|
|
835
|
+
* Check if name is a boolean check variable
|
|
836
|
+
*/
|
|
837
|
+
isBooleanCheckVariable(name) {
|
|
838
|
+
const booleanPrefixes = ['is', 'has', 'can', 'should', 'will', 'did'];
|
|
839
|
+
const nameLower = name.toLowerCase();
|
|
840
|
+
|
|
841
|
+
return booleanPrefixes.some(prefix => {
|
|
842
|
+
// Check camelCase: isValidToken, hasPassword
|
|
843
|
+
const camelPattern = new RegExp(`^${prefix}[A-Z]`);
|
|
844
|
+
// Check snake_case: is_valid_token
|
|
845
|
+
const snakePattern = new RegExp(`^${prefix}_`);
|
|
846
|
+
return camelPattern.test(name) || snakePattern.test(nameLower);
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
/**
|
|
851
|
+
* Check if name is an error constant property in error object
|
|
852
|
+
* Examples: tokenExpired, invalidToken, passwordSameAsMail, incorrectOldPassword
|
|
853
|
+
*/
|
|
854
|
+
isErrorConstantProperty(name) {
|
|
855
|
+
// Common error property patterns
|
|
856
|
+
const errorPatterns = [
|
|
857
|
+
/^invalid/i, // invalidToken, invalidRefreshToken
|
|
858
|
+
/^incorrect/i, // incorrectOldPassword, incorrectPassword
|
|
859
|
+
/^empty/i, // emptyAccessToken
|
|
860
|
+
/^missing/i, // missingPassword
|
|
861
|
+
/expired$/i, // tokenExpired, passwordExpired
|
|
862
|
+
/notfound$/i, // tokenNotFound, userNotFound
|
|
863
|
+
/notactive$/i, // tokenIsNotActive
|
|
864
|
+
/revoked$/i, // tokenHasBeenRevoked
|
|
865
|
+
/locked$/i, // userLocked, accountLocked
|
|
866
|
+
/sameas/i, // passwordSameAsMail
|
|
867
|
+
/^forgot/i, // forgotPassword
|
|
868
|
+
/^confirm.*reset/i, // confirmResetPassword
|
|
869
|
+
];
|
|
870
|
+
|
|
871
|
+
return errorPatterns.some(pattern => pattern.test(name));
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
/**
|
|
875
|
+
* Check if name is a utility function or class name
|
|
876
|
+
* Examples: generatePassword, IsPassword, validateToken, encodeJwt
|
|
877
|
+
*/
|
|
878
|
+
isUtilityFunctionOrClass(name) {
|
|
879
|
+
// Utility function prefixes
|
|
880
|
+
const utilityPrefixes = [
|
|
881
|
+
'generate', 'create', 'build', 'make',
|
|
882
|
+
'validate', 'verify', 'check',
|
|
883
|
+
'encode', 'decode', 'encrypt', 'decrypt', 'hash',
|
|
884
|
+
'extract', 'parse', 'format',
|
|
885
|
+
'get', 'set', 'update', 'delete',
|
|
886
|
+
];
|
|
887
|
+
|
|
888
|
+
// React/Vue hooks and handlers
|
|
889
|
+
const frameworkPrefixes = [
|
|
890
|
+
'use', // React hooks: useToken, usePassword, useTokenExpiration
|
|
891
|
+
'handle', // Event handlers: handleResetPassword, handleTokenChange
|
|
892
|
+
'on', // Event handlers: onPasswordChange, onTokenSubmit
|
|
893
|
+
];
|
|
894
|
+
|
|
895
|
+
// Check if it's a PascalCase class/decorator (starts with uppercase)
|
|
896
|
+
if (/^[A-Z]/.test(name)) {
|
|
897
|
+
// Decorator/Class patterns: IsPassword, ValidateToken
|
|
898
|
+
return true;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// Check if it has utility prefix
|
|
902
|
+
const nameLower = name.toLowerCase();
|
|
903
|
+
const allPrefixes = [...utilityPrefixes, ...frameworkPrefixes];
|
|
904
|
+
return allPrefixes.some(prefix => nameLower.startsWith(prefix));
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
/**
|
|
908
|
+
* Check if name is a framework-specific internal variable
|
|
909
|
+
* Examples: __next_navigation_guard_token, __webpack_*, __vite_*
|
|
910
|
+
*/
|
|
911
|
+
isFrameworkInternalVariable(name) {
|
|
912
|
+
const internalPatterns = [
|
|
913
|
+
/^__next_/i, // Next.js internals: __next_navigation_guard_token
|
|
914
|
+
/^__webpack_/i, // Webpack internals
|
|
915
|
+
/^__vite_/i, // Vite internals
|
|
916
|
+
/^__nuxt_/i, // Nuxt internals
|
|
917
|
+
/^_app/i, // Framework app internals
|
|
918
|
+
/^_document/i, // Framework document internals
|
|
919
|
+
];
|
|
920
|
+
|
|
921
|
+
return internalPatterns.some(pattern => pattern.test(name));
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
/**
|
|
925
|
+
* Check if variable is accessing framework internal property
|
|
926
|
+
* Examples: nextState?.__next_navigation_guard_token
|
|
927
|
+
*/
|
|
928
|
+
isAccessingFrameworkInternal(initText) {
|
|
929
|
+
const frameworkPropertyPatterns = [
|
|
930
|
+
/\.__next_/i, // nextState.__next_navigation_guard_token
|
|
931
|
+
/\??\.__next_/i, // nextState?.__next_navigation_guard_token
|
|
932
|
+
/\['__next_/i, // state['__next_navigation_guard_token']
|
|
933
|
+
/\["__next_/i, // state["__next_navigation_guard_token"]
|
|
934
|
+
];
|
|
935
|
+
|
|
936
|
+
return frameworkPropertyPatterns.some(pattern => pattern.test(initText));
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
/**
|
|
940
|
+
* Check if name is a React/Vue state variable or selector
|
|
941
|
+
* Examples: [token, setToken], receivedToken, selectPasswordState
|
|
942
|
+
*/
|
|
943
|
+
isStateVariableOrSelector(name) {
|
|
944
|
+
// React state patterns
|
|
945
|
+
const statePatterns = [
|
|
946
|
+
/^\[.*,\s*set/i, // [token, setToken], [password, setPassword]
|
|
947
|
+
/^set[A-Z]/, // setToken, setPassword
|
|
948
|
+
/^received[A-Z]/i, // receivedToken, receivedPassword
|
|
949
|
+
/^select.*state$/i, // selectTokenState, selectPasswordState
|
|
950
|
+
/state$/i, // tokenState, passwordState (if not part of API)
|
|
951
|
+
];
|
|
952
|
+
|
|
953
|
+
return statePatterns.some(pattern => pattern.test(name));
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
/**
|
|
957
|
+
* Check if name is a format/pattern/regex definition
|
|
958
|
+
* Examples: passwordFormat, tokenPattern, PASSWORD_REGEX
|
|
959
|
+
*/
|
|
960
|
+
isFormatOrPatternDefinition(name) {
|
|
961
|
+
const nameLower = name.toLowerCase();
|
|
962
|
+
const formatSuffixes = [
|
|
963
|
+
'format', 'pattern', 'regex', 'regexp',
|
|
964
|
+
'rule', 'rules', 'validation', 'validator',
|
|
965
|
+
'schema', 'constraint', 'character',
|
|
966
|
+
];
|
|
967
|
+
|
|
968
|
+
return formatSuffixes.some(suffix => nameLower.endsWith(suffix));
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
/**
|
|
972
|
+
* Check if variable is a URL/endpoint/path string
|
|
973
|
+
* Examples: apiGetTokenUrl = '/api/token', tokenEndpoint = 'https://...'
|
|
974
|
+
*/
|
|
975
|
+
isURLOrEndpointVariable(name, initText) {
|
|
976
|
+
const nameLower = name.toLowerCase();
|
|
977
|
+
|
|
978
|
+
// Check variable name patterns
|
|
979
|
+
const urlPatterns = [
|
|
980
|
+
'url', 'uri', 'endpoint', 'path', 'route', 'api',
|
|
981
|
+
];
|
|
982
|
+
|
|
983
|
+
const hasUrlPattern = urlPatterns.some(pattern => nameLower.includes(pattern));
|
|
984
|
+
|
|
985
|
+
if (hasUrlPattern) {
|
|
986
|
+
// Check if value looks like a URL/path
|
|
987
|
+
const urlValuePatterns = [
|
|
988
|
+
/^['"`]\//, // Starts with forward slash: '/api/token'
|
|
989
|
+
/^['"`]http/, // Starts with http/https
|
|
990
|
+
/^['"`]\.\//, // Relative path: './token'
|
|
991
|
+
/^['"`]\.\.\//, // Parent path: '../token'
|
|
992
|
+
];
|
|
993
|
+
|
|
994
|
+
return urlValuePatterns.some(pattern => pattern.test(initText.trim()));
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
return false;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
/**
|
|
1001
|
+
* Check if variable is extracting from query parameters
|
|
1002
|
+
* Examples: get(searchParams, 'token'), params.get('token'), get(params, 't', '')
|
|
1003
|
+
*/
|
|
1004
|
+
isQueryParameterExtraction(initText) {
|
|
1005
|
+
const queryPatterns = [
|
|
1006
|
+
/get<[^>]+>\(.*?,\s*['"`]\w+['"`].*?\)/, // get<Type, string>(obj, 'key', default)
|
|
1007
|
+
/\.get\(['"`]\w+['"`]\)/, // params.get('token')
|
|
1008
|
+
/searchParams\[['"`]\w+['"`]\]/, // searchParams['token']
|
|
1009
|
+
/query\[['"`]\w+['"`]\]/, // query['token']
|
|
1010
|
+
/req\.query\./, // req.query.token
|
|
1011
|
+
/router\.query\./, // router.query.token
|
|
1012
|
+
/useSearchParams/, // useSearchParams hook
|
|
1013
|
+
];
|
|
1014
|
+
|
|
1015
|
+
return queryPatterns.some(pattern => pattern.test(initText));
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
/**
|
|
1019
|
+
* Check if this is a property name in a data structure (not a hardcoded value)
|
|
1020
|
+
* Detects patterns like: { accessToken: someVariable } vs { accessToken: "hardcoded-value" }
|
|
1021
|
+
*/
|
|
1022
|
+
isPropertyNameOnly(propAssignment) {
|
|
1023
|
+
try {
|
|
1024
|
+
const initializer = propAssignment.getInitializer();
|
|
1025
|
+
if (!initializer) return true; // No value assigned, just property name
|
|
1026
|
+
|
|
1027
|
+
const initText = initializer.getText().trim();
|
|
1028
|
+
|
|
1029
|
+
// If it's a variable reference (not a string literal), it's safe
|
|
1030
|
+
// Examples: accessToken: token, password: userPassword
|
|
1031
|
+
if (!initText.match(/^['"`]/)) {
|
|
1032
|
+
return true;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
return false;
|
|
1036
|
+
} catch {
|
|
1037
|
+
return false;
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
/**
|
|
1042
|
+
* Check if this is part of an error constant object definition
|
|
1043
|
+
* Detects: const errors = { tokenExpired: { code: '...', message: '...' } }
|
|
1044
|
+
*/
|
|
1045
|
+
isPartOfErrorObject(node) {
|
|
1046
|
+
try {
|
|
1047
|
+
let parent = node.getParent();
|
|
1048
|
+
let depth = 0;
|
|
1049
|
+
const maxDepth = 5;
|
|
1050
|
+
|
|
1051
|
+
while (parent && depth < maxDepth) {
|
|
1052
|
+
// Check if parent is an object literal with error-like properties
|
|
1053
|
+
if (parent.getKind() === SyntaxKind.ObjectLiteralExpression) {
|
|
1054
|
+
const parentText = parent.getText();
|
|
1055
|
+
|
|
1056
|
+
// Check for error object patterns
|
|
1057
|
+
if (parentText.includes('code:') && parentText.includes('message:')) {
|
|
1058
|
+
return true;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
// Check for variable name containing 'error' or 'errors'
|
|
1062
|
+
const grandParent = parent.getParent();
|
|
1063
|
+
if (grandParent && grandParent.getKind() === SyntaxKind.VariableDeclaration) {
|
|
1064
|
+
const varName = grandParent.getName ? grandParent.getName() : '';
|
|
1065
|
+
if (/error/i.test(varName)) {
|
|
1066
|
+
return true;
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
parent = parent.getParent();
|
|
1072
|
+
depth++;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
return false;
|
|
1076
|
+
} catch {
|
|
1077
|
+
return false;
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
/**
|
|
1082
|
+
* Check if string literal is part of SQL query
|
|
1083
|
+
* Detects: .addSelect([...]), .select([...]), SQL column aliases
|
|
1084
|
+
*/
|
|
1085
|
+
isPartOfSQLQuery(stringLiteral) {
|
|
1086
|
+
try {
|
|
1087
|
+
let parent = stringLiteral.getParent();
|
|
1088
|
+
let depth = 0;
|
|
1089
|
+
const maxDepth = 10;
|
|
1090
|
+
|
|
1091
|
+
while (parent && depth < maxDepth) {
|
|
1092
|
+
const parentText = parent.getText();
|
|
1093
|
+
|
|
1094
|
+
// Check for SQL query method calls
|
|
1095
|
+
const sqlMethods = [
|
|
1096
|
+
'.addSelect(',
|
|
1097
|
+
'.select(',
|
|
1098
|
+
'.where(',
|
|
1099
|
+
'.andWhere(',
|
|
1100
|
+
'.orWhere(',
|
|
1101
|
+
'.orderBy(',
|
|
1102
|
+
'.groupBy(',
|
|
1103
|
+
'.having(',
|
|
1104
|
+
'.leftJoin(',
|
|
1105
|
+
'.innerJoin(',
|
|
1106
|
+
'.rightJoin(',
|
|
1107
|
+
'createQueryBuilder(',
|
|
1108
|
+
'.from(',
|
|
1109
|
+
'.into(',
|
|
1110
|
+
];
|
|
1111
|
+
|
|
1112
|
+
if (sqlMethods.some(method => parentText.includes(method))) {
|
|
1113
|
+
return true;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
// Check if inside array literal passed to SQL methods
|
|
1117
|
+
if (parent.getKind() === SyntaxKind.ArrayLiteralExpression) {
|
|
1118
|
+
const arrayParent = parent.getParent();
|
|
1119
|
+
if (arrayParent) {
|
|
1120
|
+
const arrayParentText = arrayParent.getText();
|
|
1121
|
+
if (sqlMethods.some(method => arrayParentText.includes(method))) {
|
|
1122
|
+
return true;
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
// Check for SQL alias pattern: "table.column as alias"
|
|
1128
|
+
const literalValue = stringLiteral.getLiteralValue();
|
|
1129
|
+
if (literalValue && /\s+as\s+["']?\w+["']?/i.test(literalValue)) {
|
|
1130
|
+
return true;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
parent = parent.getParent();
|
|
1134
|
+
depth++;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
return false;
|
|
1138
|
+
} catch {
|
|
1139
|
+
return false;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
/**
|
|
1144
|
+
* Check if variable is extracting data from API response
|
|
1145
|
+
* Detects: response.data.token, (res.payload as Type).token
|
|
1146
|
+
*/
|
|
1147
|
+
isExtractingFromAPIResponse(initText) {
|
|
1148
|
+
const apiResponsePatterns = [
|
|
1149
|
+
/\.data\[['"].*['"]\]\./, // .data['AuthenticationResult'].
|
|
1150
|
+
/\.data\.\w+\./, // .data.AuthenticationResult.
|
|
1151
|
+
/\.payload\s+as\s+\w+\)\.\w+/, // (res.payload as Type).token
|
|
1152
|
+
/response\.\w+/, // response.token, response.AccessToken
|
|
1153
|
+
/result\.\w+/, // result.token, result.RefreshToken
|
|
1154
|
+
/authResult\./, // authResult.data.
|
|
1155
|
+
/loginInCognito\./, // loginInCognito.data.
|
|
1156
|
+
/\.AuthenticationResult\./, // .AuthenticationResult.AccessToken
|
|
1157
|
+
/\.Credentials\./, // .Credentials.AccessKeyId
|
|
1158
|
+
/res\.payload/, // res.payload.token
|
|
1159
|
+
];
|
|
1160
|
+
|
|
1161
|
+
return apiResponsePatterns.some(pattern => pattern.test(initText));
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
/**
|
|
1165
|
+
* Check if object literal only contains references (no hardcoded values)
|
|
1166
|
+
* Returns true if ALL property values are variables/function calls, not string literals
|
|
1167
|
+
*/
|
|
1168
|
+
isObjectLiteralWithOnlyReferences(initializer) {
|
|
1169
|
+
try {
|
|
1170
|
+
if (initializer.getKind() !== SyntaxKind.ObjectLiteralExpression) {
|
|
1171
|
+
return false;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
const properties = initializer.getProperties();
|
|
1175
|
+
let hasHardcodedString = false;
|
|
1176
|
+
|
|
1177
|
+
for (const prop of properties) {
|
|
1178
|
+
if (prop.getKind() === SyntaxKind.PropertyAssignment) {
|
|
1179
|
+
const propInit = prop.getInitializer();
|
|
1180
|
+
if (propInit) {
|
|
1181
|
+
const propText = propInit.getText().trim();
|
|
1182
|
+
|
|
1183
|
+
// If it's a string literal (not a variable), mark as having hardcoded string
|
|
1184
|
+
if (propText.match(/^['"`]/) && !propText.match(/^['"`]\s*$/)) {
|
|
1185
|
+
// Check if it's a short descriptive string (likely not a secret)
|
|
1186
|
+
const cleanValue = propText.replace(/^['"`]|['"`]$/g, '');
|
|
1187
|
+
if (cleanValue.length > this.minSecretLength &&
|
|
1188
|
+
!this.isMessageString(propText)) {
|
|
1189
|
+
hasHardcodedString = true;
|
|
1190
|
+
break;
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
return !hasHardcodedString;
|
|
1198
|
+
} catch {
|
|
1199
|
+
return false;
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
module.exports = S012SymbolBasedAnalyzer;
|