@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,592 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S004 - Sensitive Data Logging Protection (Symbol-based Analyzer)
|
|
3
|
+
*
|
|
4
|
+
* Detects logging of sensitive information like passwords, tokens, credit cards
|
|
5
|
+
* without proper redaction or masking.
|
|
6
|
+
*
|
|
7
|
+
* Based on:
|
|
8
|
+
* - OWASP A09:2021 - Security Logging and Monitoring Failures
|
|
9
|
+
* - CWE-532: Insertion of Sensitive Information into Log File
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const { SyntaxKind } = require("ts-morph");
|
|
13
|
+
|
|
14
|
+
class S004SymbolBasedAnalyzer {
|
|
15
|
+
constructor(semanticEngine = null) {
|
|
16
|
+
this.ruleId = "S004";
|
|
17
|
+
this.semanticEngine = semanticEngine;
|
|
18
|
+
|
|
19
|
+
// Logging function patterns (common across frameworks)
|
|
20
|
+
this.loggingFunctions = [
|
|
21
|
+
"console.log",
|
|
22
|
+
"console.info",
|
|
23
|
+
"console.warn",
|
|
24
|
+
"console.error",
|
|
25
|
+
"console.debug",
|
|
26
|
+
"logger.log",
|
|
27
|
+
"logger.info",
|
|
28
|
+
"logger.warn",
|
|
29
|
+
"logger.error",
|
|
30
|
+
"logger.debug",
|
|
31
|
+
"log.info",
|
|
32
|
+
"log.warn",
|
|
33
|
+
"log.error",
|
|
34
|
+
"log.debug",
|
|
35
|
+
"winston.log",
|
|
36
|
+
"winston.info",
|
|
37
|
+
"winston.error",
|
|
38
|
+
"pino.info",
|
|
39
|
+
"pino.error",
|
|
40
|
+
"bunyan.info",
|
|
41
|
+
"bunyan.error",
|
|
42
|
+
"print", // Python
|
|
43
|
+
"logging.info", // Python
|
|
44
|
+
"logging.error",
|
|
45
|
+
"log.printf", // Go
|
|
46
|
+
"log.println",
|
|
47
|
+
"logger.info", // Go/Java
|
|
48
|
+
"logger.error",
|
|
49
|
+
"system.out.println", // Java
|
|
50
|
+
"nslog", // Objective-C
|
|
51
|
+
"os_log", // Swift
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
// Sensitive field patterns (field names that indicate sensitive data)
|
|
55
|
+
this.sensitiveFieldPatterns = [
|
|
56
|
+
// Authentication & Authorization
|
|
57
|
+
"password",
|
|
58
|
+
"passwd",
|
|
59
|
+
"pwd",
|
|
60
|
+
"secret",
|
|
61
|
+
"secretkey",
|
|
62
|
+
"secret_key",
|
|
63
|
+
"apikey",
|
|
64
|
+
"api_key",
|
|
65
|
+
"access_token",
|
|
66
|
+
"accesstoken",
|
|
67
|
+
"refresh_token",
|
|
68
|
+
"refreshtoken",
|
|
69
|
+
"auth_token",
|
|
70
|
+
"authtoken",
|
|
71
|
+
"bearer",
|
|
72
|
+
"authorization",
|
|
73
|
+
"session",
|
|
74
|
+
"sessionid",
|
|
75
|
+
"session_id",
|
|
76
|
+
"jwt",
|
|
77
|
+
"token",
|
|
78
|
+
"otp",
|
|
79
|
+
"pin",
|
|
80
|
+
"privatekey",
|
|
81
|
+
"private_key",
|
|
82
|
+
"credentials",
|
|
83
|
+
"credential",
|
|
84
|
+
|
|
85
|
+
// Payment & Financial
|
|
86
|
+
"credit_card",
|
|
87
|
+
"creditcard",
|
|
88
|
+
"cardnumber",
|
|
89
|
+
"card_number",
|
|
90
|
+
"cvv",
|
|
91
|
+
"cvc",
|
|
92
|
+
"ccv",
|
|
93
|
+
"card_cvv",
|
|
94
|
+
"expiry",
|
|
95
|
+
"expiration",
|
|
96
|
+
"pan", // Primary Account Number
|
|
97
|
+
"iban",
|
|
98
|
+
"account_number",
|
|
99
|
+
"accountnumber",
|
|
100
|
+
"routing_number",
|
|
101
|
+
"bank_account",
|
|
102
|
+
"ssn", // Social Security Number
|
|
103
|
+
"tax_id",
|
|
104
|
+
|
|
105
|
+
// Personal Information
|
|
106
|
+
"email",
|
|
107
|
+
"phone",
|
|
108
|
+
"phonenumber",
|
|
109
|
+
"phone_number",
|
|
110
|
+
"address",
|
|
111
|
+
"birthday",
|
|
112
|
+
"birth_date",
|
|
113
|
+
"dob",
|
|
114
|
+
"passport",
|
|
115
|
+
"license",
|
|
116
|
+
"idcard",
|
|
117
|
+
"id_card",
|
|
118
|
+
"national_id",
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
// Patterns that indicate the data is already masked/redacted (safe to log)
|
|
122
|
+
this.safeLoggingPatterns = [
|
|
123
|
+
"masked",
|
|
124
|
+
"redacted",
|
|
125
|
+
"sanitized",
|
|
126
|
+
"filtered",
|
|
127
|
+
"hashed",
|
|
128
|
+
"encrypted",
|
|
129
|
+
"****",
|
|
130
|
+
"***",
|
|
131
|
+
"...",
|
|
132
|
+
"[redacted]",
|
|
133
|
+
"[masked]",
|
|
134
|
+
".mask(",
|
|
135
|
+
".redact(",
|
|
136
|
+
".sanitize(",
|
|
137
|
+
".hash(",
|
|
138
|
+
".encrypt(",
|
|
139
|
+
".replace(/", // password.replace(/./g, '*')
|
|
140
|
+
".replace(", // password.replace('', '')
|
|
141
|
+
"replacewith", // password.replaceWith('***')
|
|
142
|
+
"substr(0,", // Partial masking: token.substr(0, 4) + '****'
|
|
143
|
+
"substring(0,",
|
|
144
|
+
"slice(0,",
|
|
145
|
+
];
|
|
146
|
+
|
|
147
|
+
// Patterns that indicate selective field logging (safe approach)
|
|
148
|
+
this.selectiveLoggingPatterns = [
|
|
149
|
+
"omit(",
|
|
150
|
+
"pick(",
|
|
151
|
+
"exclude(",
|
|
152
|
+
"without(",
|
|
153
|
+
"except(",
|
|
154
|
+
".filter(",
|
|
155
|
+
"filterkeys",
|
|
156
|
+
"filter_keys",
|
|
157
|
+
];
|
|
158
|
+
|
|
159
|
+
// Objects/structures that commonly contain sensitive data
|
|
160
|
+
this.sensitiveBulkPatterns = [
|
|
161
|
+
"req.body",
|
|
162
|
+
"request.body",
|
|
163
|
+
"req.headers",
|
|
164
|
+
"request.headers",
|
|
165
|
+
"formdata",
|
|
166
|
+
"form-data",
|
|
167
|
+
"req.query",
|
|
168
|
+
"req.params",
|
|
169
|
+
"credentials",
|
|
170
|
+
"auth",
|
|
171
|
+
"payment",
|
|
172
|
+
"paymentinfo",
|
|
173
|
+
"payment_info",
|
|
174
|
+
"userdata",
|
|
175
|
+
"user_data",
|
|
176
|
+
"profile",
|
|
177
|
+
];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Main analysis method
|
|
182
|
+
*/
|
|
183
|
+
analyze(sourceFile) {
|
|
184
|
+
const violations = [];
|
|
185
|
+
|
|
186
|
+
// Check for logging function calls
|
|
187
|
+
const callExpressions = sourceFile.getDescendantsOfKind(
|
|
188
|
+
SyntaxKind.CallExpression
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
for (const callExpr of callExpressions) {
|
|
192
|
+
const callText = callExpr.getText().toLowerCase();
|
|
193
|
+
|
|
194
|
+
// Check if this is a logging function call
|
|
195
|
+
if (!this.isLoggingFunction(callText)) {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Get the arguments being logged
|
|
200
|
+
const args = callExpr.getArguments();
|
|
201
|
+
if (args.length === 0) {
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Check each argument for sensitive data
|
|
206
|
+
for (const arg of args) {
|
|
207
|
+
const argText = arg.getText();
|
|
208
|
+
const argTextLower = argText.toLowerCase();
|
|
209
|
+
|
|
210
|
+
// Skip if already masked/redacted
|
|
211
|
+
if (this.isSafelyMasked(argTextLower)) {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Skip if using selective logging (omit/pick specific fields)
|
|
216
|
+
if (this.isSelectiveLogging(argTextLower)) {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Check for sensitive field names
|
|
221
|
+
const sensitiveField = this.findSensitiveField(argTextLower);
|
|
222
|
+
if (sensitiveField) {
|
|
223
|
+
// Context-aware filtering for common false positives
|
|
224
|
+
let shouldSkip = false;
|
|
225
|
+
|
|
226
|
+
// Skip if keyword appears in uppercase in message (e.g., "NO EMAIL ERROR")
|
|
227
|
+
if (this.isUppercaseInMessage(sensitiveField, argText)) {
|
|
228
|
+
shouldSkip = true;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Skip if pattern is in a message string (e.g., "no session", "session is null")
|
|
232
|
+
if (this.isInMessageString(sensitiveField, argText)) {
|
|
233
|
+
shouldSkip = true;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Skip if it's selective property access of non-sensitive fields
|
|
237
|
+
// e.g., credentials.type, payment.id, paymentIntentId
|
|
238
|
+
if (sensitiveField === 'credentials' && argTextLower.includes('credentials.type')) {
|
|
239
|
+
shouldSkip = true;
|
|
240
|
+
}
|
|
241
|
+
if (sensitiveField === 'payment') {
|
|
242
|
+
// Skip if it's paymentIntentId, paymentId, or payment.id (non-sensitive IDs)
|
|
243
|
+
if (argTextLower.includes('paymentintentid') ||
|
|
244
|
+
argTextLower.includes('paymentid') ||
|
|
245
|
+
argTextLower.includes('payment.id') ||
|
|
246
|
+
argTextLower.includes('payment.status') ||
|
|
247
|
+
// Check if "payment" appears only as part of a safe compound word
|
|
248
|
+
(argTextLower.includes('payment') &&
|
|
249
|
+
!argTextLower.includes('paymentdata') &&
|
|
250
|
+
!argTextLower.includes('paymentinfo') &&
|
|
251
|
+
(argTextLower.match(/payment[a-z]+id/i) || // paymentIntentId, paymentMethodId
|
|
252
|
+
argTextLower.match(/payment[a-z]+status/i) || // paymentStatus
|
|
253
|
+
argTextLower.match(/payment[a-z]+type/i)))) { // paymentType
|
|
254
|
+
shouldSkip = true;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (!shouldSkip) {
|
|
259
|
+
violations.push({
|
|
260
|
+
line: callExpr.getStartLineNumber(),
|
|
261
|
+
column: callExpr.getStart() - callExpr.getStartLinePos(),
|
|
262
|
+
message: `Sensitive data logging: Logging '${argText.substring(0, 80)}${argText.length > 80 ? '...' : ''}' may expose sensitive field '${sensitiveField}' - use masking/redaction before logging`,
|
|
263
|
+
severity: "warning",
|
|
264
|
+
ruleId: this.ruleId,
|
|
265
|
+
});
|
|
266
|
+
break; // One violation per log statement is enough
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Check for bulk sensitive objects (req.body, req.headers, etc.)
|
|
271
|
+
const sensitiveBulk = this.findSensitiveBulkPattern(argTextLower, argText);
|
|
272
|
+
if (sensitiveBulk) {
|
|
273
|
+
violations.push({
|
|
274
|
+
line: callExpr.getStartLineNumber(),
|
|
275
|
+
column: callExpr.getStart() - callExpr.getStartLinePos(),
|
|
276
|
+
message: `Sensitive data logging: Logging '${sensitiveBulk}' may expose sensitive fields like passwords, tokens, or payment data - use selective field logging or redaction`,
|
|
277
|
+
severity: "warning",
|
|
278
|
+
ruleId: this.ruleId,
|
|
279
|
+
});
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Check for template literals containing sensitive fields
|
|
284
|
+
if (this.containsSensitiveFieldInTemplate(argText)) {
|
|
285
|
+
const field = this.findSensitiveFieldInTemplate(argText);
|
|
286
|
+
violations.push({
|
|
287
|
+
line: callExpr.getStartLineNumber(),
|
|
288
|
+
column: callExpr.getStart() - callExpr.getStartLinePos(),
|
|
289
|
+
message: `Sensitive data logging: Template literal contains sensitive field '${field}' - mask before logging`,
|
|
290
|
+
severity: "warning",
|
|
291
|
+
ruleId: this.ruleId,
|
|
292
|
+
});
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return violations;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Check if the function call is a logging function
|
|
303
|
+
*/
|
|
304
|
+
isLoggingFunction(text) {
|
|
305
|
+
return this.loggingFunctions.some((func) => text.includes(func));
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Check if the data is safely masked/redacted
|
|
310
|
+
*/
|
|
311
|
+
isSafelyMasked(text) {
|
|
312
|
+
// Check for masking patterns
|
|
313
|
+
if (this.safeLoggingPatterns.some((pattern) => text.includes(pattern))) {
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Check if it's just a string literal message (not actual data)
|
|
318
|
+
// e.g., 'Authenticating with token:' is just a label, not the token itself
|
|
319
|
+
if (text.startsWith("'") || text.startsWith('"') || text.startsWith('`')) {
|
|
320
|
+
// It's a string literal - check if it's just a message/label
|
|
321
|
+
// Look for patterns like 'message: ${var}' or 'message:', var
|
|
322
|
+
// If the string literal itself doesn't contain variables and is just text, it's safe
|
|
323
|
+
if (!text.includes('${') && text.length < 100) {
|
|
324
|
+
return true; // It's just a log message label
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Check if using selective logging (omit/pick specific fields)
|
|
333
|
+
*/
|
|
334
|
+
isSelectiveLogging(text) {
|
|
335
|
+
return this.selectiveLoggingPatterns.some((pattern) => text.includes(pattern));
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Find sensitive field name in the text
|
|
340
|
+
*/
|
|
341
|
+
findSensitiveField(text) {
|
|
342
|
+
// Remove comments from text before checking
|
|
343
|
+
const textWithoutComments = text.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
344
|
+
|
|
345
|
+
for (const pattern of this.sensitiveFieldPatterns) {
|
|
346
|
+
// Match whole words or property access patterns
|
|
347
|
+
const regex = new RegExp(
|
|
348
|
+
`\\b${pattern}\\b|["'\`]${pattern}["'\`]|\\.${pattern}\\b|\\[["'\`]${pattern}["'\`]\\]`,
|
|
349
|
+
"i"
|
|
350
|
+
);
|
|
351
|
+
if (regex.test(textWithoutComments)) {
|
|
352
|
+
return pattern;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Find sensitive bulk object pattern (req.body, req.headers, etc.)
|
|
360
|
+
* Now with context-awareness to reduce false positives
|
|
361
|
+
*/
|
|
362
|
+
findSensitiveBulkPattern(textLower, originalText) {
|
|
363
|
+
for (const pattern of this.sensitiveBulkPatterns) {
|
|
364
|
+
if (!textLower.includes(pattern)) {
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Check if it's selective field access like req.headers['user-agent']
|
|
369
|
+
if (pattern === 'req.headers' || pattern === 'request.headers') {
|
|
370
|
+
if (textLower.includes("['user-agent']") ||
|
|
371
|
+
textLower.includes("['content-type']") ||
|
|
372
|
+
textLower.includes("['accept']") ||
|
|
373
|
+
textLower.includes("['referer']") ||
|
|
374
|
+
textLower.includes("[\"user-agent\"]") ||
|
|
375
|
+
textLower.includes("[\"content-type\"]") ||
|
|
376
|
+
textLower.includes("[\"accept\"]") ||
|
|
377
|
+
textLower.includes("[\"referer\"]") ||
|
|
378
|
+
textLower.includes(".get('user-agent')") ||
|
|
379
|
+
textLower.includes(".get('content-type')") ||
|
|
380
|
+
textLower.includes(".get('accept')") ||
|
|
381
|
+
textLower.includes(".get('referer')")) {
|
|
382
|
+
continue; // Skip, it's safe
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Skip if pattern appears ONLY in a pure string literal (no variables)
|
|
387
|
+
// e.g., 'Send email success' or "Payment completed" are just messages
|
|
388
|
+
if (this.isOnlyInStringLiteral(pattern, originalText)) {
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// For "payment" and "auth" patterns, be more strict
|
|
393
|
+
if (pattern === 'payment' || pattern === 'auth' || pattern === 'credentials') {
|
|
394
|
+
// Skip if it's inside a log message string literal
|
|
395
|
+
// Pattern: logger.log('Message with payment/auth word')
|
|
396
|
+
if (this.isInMessageString(pattern, originalText)) {
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return pattern;
|
|
402
|
+
}
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Check if pattern appears ONLY inside string literals (not as variables)
|
|
408
|
+
* e.g., 'Send email to user' vs logger.log(email)
|
|
409
|
+
*/
|
|
410
|
+
isOnlyInStringLiteral(pattern, text) {
|
|
411
|
+
// Check if text is a pure string literal without interpolation
|
|
412
|
+
if ((text.startsWith("'") && text.endsWith("'") && !text.includes('${')) ||
|
|
413
|
+
(text.startsWith('"') && text.endsWith('"') && !text.includes('${'))) {
|
|
414
|
+
return true;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Check if pattern appears in template literal but not in variables
|
|
418
|
+
// Template: `Message with ${var}` - pattern in "Message" part is safe
|
|
419
|
+
if (text.includes('`')) {
|
|
420
|
+
// Extract template string parts (not inside ${})
|
|
421
|
+
const withoutVars = text.replace(/\$\{[^}]+\}/g, '');
|
|
422
|
+
if (withoutVars.toLowerCase().includes(pattern) && !text.match(new RegExp(`\\$\\{[^}]*${pattern}`, 'i'))) {
|
|
423
|
+
return true;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return false;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Check if pattern appears in uppercase in a message string
|
|
432
|
+
* e.g., "NO EMAIL ERROR" vs logger.log(email)
|
|
433
|
+
*/
|
|
434
|
+
isUppercaseInMessage(pattern, text) {
|
|
435
|
+
const upperPattern = pattern.toUpperCase();
|
|
436
|
+
|
|
437
|
+
// Check if pattern appears in uppercase and surrounded by other uppercase words
|
|
438
|
+
// This suggests it's part of a message constant, not actual data
|
|
439
|
+
if (text.includes(upperPattern)) {
|
|
440
|
+
// Extract words around the uppercase pattern
|
|
441
|
+
const regex = new RegExp(`([A-Z_]+\\s+)?${upperPattern}(\\s+[A-Z_]+)?`);
|
|
442
|
+
const match = text.match(regex);
|
|
443
|
+
if (match) {
|
|
444
|
+
// If surrounded by other uppercase words, it's likely a message
|
|
445
|
+
return true;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return false;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Check if pattern is inside a message string (not actual data)
|
|
454
|
+
* e.g., "Error in payment processing" vs logger.log(paymentData)
|
|
455
|
+
*/
|
|
456
|
+
isInMessageString(pattern, text) {
|
|
457
|
+
const lowerText = text.toLowerCase();
|
|
458
|
+
|
|
459
|
+
// Common message patterns that are safe
|
|
460
|
+
const safeMessagePatterns = [
|
|
461
|
+
`${pattern} processing`,
|
|
462
|
+
`${pattern} completed`,
|
|
463
|
+
`${pattern} failed`,
|
|
464
|
+
`${pattern} success`,
|
|
465
|
+
`${pattern} error`,
|
|
466
|
+
`${pattern} schedule`,
|
|
467
|
+
`confirm ${pattern}`,
|
|
468
|
+
`send ${pattern}`,
|
|
469
|
+
`update ${pattern}`,
|
|
470
|
+
`${pattern} service`,
|
|
471
|
+
`${pattern} notice`,
|
|
472
|
+
`no ${pattern}`, // "no email error"
|
|
473
|
+
`${pattern} is null`, // "session is null"
|
|
474
|
+
`${pattern} not found`, // "session not found"
|
|
475
|
+
`${pattern} is missing`, // "token is missing"
|
|
476
|
+
`verify ${pattern}`, // "verify email"
|
|
477
|
+
`could not verify ${pattern}`, // "could not verify email"
|
|
478
|
+
`could update ${pattern}`, // "could update secondary email"
|
|
479
|
+
`could not update ${pattern}`, // "could not update email"
|
|
480
|
+
`${pattern} of the user`, // "email of the user"
|
|
481
|
+
`${pattern} points`, // "expiration points"
|
|
482
|
+
`${pattern} scan`, // "expiration scan"
|
|
483
|
+
`updated ${pattern}`, // "updated expiration points"
|
|
484
|
+
`end user`, // "end user X expiration"
|
|
485
|
+
];
|
|
486
|
+
|
|
487
|
+
if (safeMessagePatterns.some(msg => lowerText.includes(msg))) {
|
|
488
|
+
return true;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Check for patterns like "send {pattern} to user" (e.g., "send email to user")
|
|
492
|
+
if (lowerText.includes(`send ${pattern} to`) ||
|
|
493
|
+
lowerText.includes(`sent ${pattern} to`)) {
|
|
494
|
+
return true;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Check for patterns with words in between (e.g., "could update secondary email")
|
|
498
|
+
if (lowerText.includes('could') && lowerText.includes('update') &&
|
|
499
|
+
lowerText.includes(pattern) && lowerText.includes('cause')) {
|
|
500
|
+
return true;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Check for patterns like "could not" with action verbs
|
|
504
|
+
if (lowerText.includes(`could not`) &&
|
|
505
|
+
(lowerText.includes('verify') || lowerText.includes('update') ||
|
|
506
|
+
lowerText.includes('change') || lowerText.includes('set')) &&
|
|
507
|
+
lowerText.includes(pattern)) {
|
|
508
|
+
return true;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Check for URL paths (e.g., '/api/get-impersonate-token')
|
|
512
|
+
if (lowerText.includes('/api/') || lowerText.includes('/auth/')) {
|
|
513
|
+
return true;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Check for localStorage/sessionStorage keys (e.g., 'auth_timestamp')
|
|
517
|
+
if ((lowerText.includes("'") || lowerText.includes('"')) &&
|
|
518
|
+
(lowerText.includes('_timestamp') || lowerText.includes('_key') || lowerText.includes('_storage'))) {
|
|
519
|
+
return true;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Check for error message constants (e.g., 'BE_E_ActionDeniedDueToUnauthorized')
|
|
523
|
+
if (lowerText.includes('error') && lowerText.includes('_e_')) {
|
|
524
|
+
return true;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Check for common words containing the pattern as substring
|
|
528
|
+
// e.g., "Authentication required" contains "auth" but isn't logging auth data
|
|
529
|
+
const safeSubstrings = [
|
|
530
|
+
'authentication',
|
|
531
|
+
'authorization',
|
|
532
|
+
'authenticate',
|
|
533
|
+
'authorized',
|
|
534
|
+
'unauthorized',
|
|
535
|
+
'authenticating',
|
|
536
|
+
];
|
|
537
|
+
if (safeSubstrings.some(word => lowerText.includes(word))) {
|
|
538
|
+
return true;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
return false;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Check if template literal contains sensitive fields
|
|
546
|
+
*/
|
|
547
|
+
containsSensitiveFieldInTemplate(text) {
|
|
548
|
+
// Check if it's a template literal with interpolation
|
|
549
|
+
if (!text.includes('${') || !text.includes('}')) {
|
|
550
|
+
return false;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Extract variable names from template
|
|
554
|
+
const varMatches = text.match(/\$\{([^}]+)\}/g);
|
|
555
|
+
if (!varMatches) {
|
|
556
|
+
return false;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
for (const varMatch of varMatches) {
|
|
560
|
+
const varContent = varMatch.slice(2, -1).toLowerCase();
|
|
561
|
+
|
|
562
|
+
// Check if variable name contains sensitive field
|
|
563
|
+
if (this.findSensitiveField(varContent)) {
|
|
564
|
+
return true;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
return false;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Find which sensitive field is in the template
|
|
573
|
+
*/
|
|
574
|
+
findSensitiveFieldInTemplate(text) {
|
|
575
|
+
const varMatches = text.match(/\$\{([^}]+)\}/g);
|
|
576
|
+
if (!varMatches) {
|
|
577
|
+
return null;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
for (const varMatch of varMatches) {
|
|
581
|
+
const varContent = varMatch.slice(2, -1).toLowerCase();
|
|
582
|
+
const field = this.findSensitiveField(varContent);
|
|
583
|
+
if (field) {
|
|
584
|
+
return varContent;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
return null;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
module.exports = S004SymbolBasedAnalyzer;
|