@sun-asterisk/sunlint 1.3.18 → 1.3.19
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 +77 -18
- package/core/cli-program.js +2 -1
- package/core/github-annotate-service.js +89 -0
- package/core/output-service.js +25 -0
- package/core/summary-report-service.js +30 -30
- package/package.json +3 -2
- package/rules/common/C014_dependency_injection/symbol-based-analyzer.js +392 -280
- package/rules/common/C017_constructor_logic/analyzer.js +137 -503
- package/rules/common/C017_constructor_logic/config.json +50 -0
- package/rules/common/C017_constructor_logic/symbol-based-analyzer.js +463 -0
- package/rules/security/S006_no_plaintext_recovery_codes/symbol-based-analyzer.js +463 -21
- package/rules/security/S011_secure_guid_generation/README.md +255 -0
- package/rules/security/S011_secure_guid_generation/analyzer.js +135 -0
- package/rules/security/S011_secure_guid_generation/config.json +56 -0
- package/rules/security/S011_secure_guid_generation/symbol-based-analyzer.js +609 -0
- package/rules/security/S028_file_upload_size_limits/README.md +537 -0
- package/rules/security/S028_file_upload_size_limits/analyzer.js +202 -0
- package/rules/security/S028_file_upload_size_limits/config.json +186 -0
- package/rules/security/S028_file_upload_size_limits/symbol-based-analyzer.js +530 -0
- package/rules/security/S041_session_token_invalidation/README.md +303 -0
- package/rules/security/S041_session_token_invalidation/analyzer.js +242 -0
- package/rules/security/S041_session_token_invalidation/config.json +175 -0
- package/rules/security/S041_session_token_invalidation/regex-based-analyzer.js +411 -0
- package/rules/security/S041_session_token_invalidation/symbol-based-analyzer.js +674 -0
- package/rules/security/S044_re_authentication_required/README.md +136 -0
- package/rules/security/S044_re_authentication_required/analyzer.js +242 -0
- package/rules/security/S044_re_authentication_required/config.json +161 -0
- package/rules/security/S044_re_authentication_required/regex-based-analyzer.js +329 -0
- package/rules/security/S044_re_authentication_required/symbol-based-analyzer.js +537 -0
- package/rules/security/S045_brute_force_protection/README.md +345 -0
- package/rules/security/S045_brute_force_protection/analyzer.js +336 -0
- package/rules/security/S045_brute_force_protection/config.json +139 -0
- package/rules/security/S045_brute_force_protection/symbol-based-analyzer.js +646 -0
- package/rules/common/C017_constructor_logic/semantic-analyzer.js +0 -340
|
@@ -0,0 +1,609 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S011 - Secure GUID Generation (Symbol-based Analyzer)
|
|
3
|
+
*
|
|
4
|
+
* Detects weak or predictable GUID/UUID generation methods used for security purposes.
|
|
5
|
+
* Security-critical GUIDs must use UUID v4 with CSPRNG.
|
|
6
|
+
*
|
|
7
|
+
* Based on:
|
|
8
|
+
* - OWASP A02:2021 - Cryptographic Failures
|
|
9
|
+
* - CWE-338: Use of Cryptographically Weak Pseudo-Random Number Generator (PRNG)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const { SyntaxKind } = require("ts-morph");
|
|
13
|
+
|
|
14
|
+
class S011SymbolBasedAnalyzer {
|
|
15
|
+
constructor(semanticEngine = null) {
|
|
16
|
+
this.ruleId = "S011";
|
|
17
|
+
this.semanticEngine = semanticEngine;
|
|
18
|
+
|
|
19
|
+
// Security-related variable name patterns
|
|
20
|
+
this.securityKeywords = [
|
|
21
|
+
"session",
|
|
22
|
+
"token",
|
|
23
|
+
"apikey",
|
|
24
|
+
"api_key",
|
|
25
|
+
"resettoken",
|
|
26
|
+
"reset_token",
|
|
27
|
+
"authtoken",
|
|
28
|
+
"auth_token",
|
|
29
|
+
"accesstoken",
|
|
30
|
+
"access_token",
|
|
31
|
+
"refreshtoken",
|
|
32
|
+
"refresh_token",
|
|
33
|
+
"verificationtoken",
|
|
34
|
+
"verification_token",
|
|
35
|
+
"activationtoken",
|
|
36
|
+
"activation_token",
|
|
37
|
+
"secret",
|
|
38
|
+
"credential",
|
|
39
|
+
"password",
|
|
40
|
+
"otp",
|
|
41
|
+
"nonce",
|
|
42
|
+
"csrf",
|
|
43
|
+
"jwt",
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
// Weak/unsafe GUID generation patterns
|
|
47
|
+
this.unsafeMethods = [
|
|
48
|
+
"math.random",
|
|
49
|
+
"date.now",
|
|
50
|
+
"new date().gettime",
|
|
51
|
+
"uuidv1",
|
|
52
|
+
"uuid.v1",
|
|
53
|
+
"v1(",
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
// Timestamp/time utility functions (NOT random generation - business logic)
|
|
57
|
+
this.timeUtilityPatterns = [
|
|
58
|
+
"getcurrenttimestamp",
|
|
59
|
+
"gettimestamp",
|
|
60
|
+
"getstartof",
|
|
61
|
+
"getendof",
|
|
62
|
+
"timestampto",
|
|
63
|
+
"totimestamp",
|
|
64
|
+
"datetotimestamp",
|
|
65
|
+
"moment(",
|
|
66
|
+
"dayjs(",
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
// Safe GUID generation patterns (CSPRNG-based)
|
|
70
|
+
this.safeMethods = [
|
|
71
|
+
"crypto.randomuuid",
|
|
72
|
+
"randomuuid",
|
|
73
|
+
"crypto.randombytes",
|
|
74
|
+
"randombytes",
|
|
75
|
+
"uuidv4",
|
|
76
|
+
"uuid.v4",
|
|
77
|
+
"v4(",
|
|
78
|
+
"securerandom",
|
|
79
|
+
"secrets.token",
|
|
80
|
+
"guid.newguid", // .NET
|
|
81
|
+
"uuid.randomuuid", // Java
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
// Safe contexts where weak random is acceptable
|
|
85
|
+
this.safeContexts = [
|
|
86
|
+
"test",
|
|
87
|
+
"mock",
|
|
88
|
+
"stub",
|
|
89
|
+
"fixture",
|
|
90
|
+
"example",
|
|
91
|
+
"demo",
|
|
92
|
+
"sample",
|
|
93
|
+
"display",
|
|
94
|
+
"temp",
|
|
95
|
+
"temporary",
|
|
96
|
+
"ui",
|
|
97
|
+
"client",
|
|
98
|
+
"view",
|
|
99
|
+
"order", // Business IDs
|
|
100
|
+
"invoice",
|
|
101
|
+
"transaction",
|
|
102
|
+
"record",
|
|
103
|
+
"trace", // Tracing/logging IDs
|
|
104
|
+
"request",
|
|
105
|
+
"correlation",
|
|
106
|
+
];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async initialize(semanticEngine) {
|
|
110
|
+
this.semanticEngine = semanticEngine;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async analyze(sourceFile, filePath) {
|
|
114
|
+
const violations = [];
|
|
115
|
+
const reportedLines = new Set(); // Track reported lines to avoid duplicates
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
// Check variable declarations with weak GUID generation (highest priority)
|
|
119
|
+
this.checkVariableDeclarations(
|
|
120
|
+
sourceFile,
|
|
121
|
+
filePath,
|
|
122
|
+
violations,
|
|
123
|
+
reportedLines
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
// Check call expressions only if not already reported at variable level
|
|
127
|
+
this.checkCallExpressions(
|
|
128
|
+
sourceFile,
|
|
129
|
+
filePath,
|
|
130
|
+
violations,
|
|
131
|
+
reportedLines
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
// Check custom GUID generation functions
|
|
135
|
+
this.checkCustomGuidFunctions(
|
|
136
|
+
sourceFile,
|
|
137
|
+
filePath,
|
|
138
|
+
violations,
|
|
139
|
+
reportedLines
|
|
140
|
+
);
|
|
141
|
+
} catch (error) {
|
|
142
|
+
console.warn(`⚠ [S011] Analysis error in ${filePath}: ${error.message}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return violations;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Check variable declarations for weak GUID generation
|
|
150
|
+
* e.g., const sessionId = Math.random().toString(36)
|
|
151
|
+
*/
|
|
152
|
+
checkVariableDeclarations(sourceFile, filePath, violations, reportedLines) {
|
|
153
|
+
const varDeclarations = sourceFile.getDescendantsOfKind(
|
|
154
|
+
SyntaxKind.VariableDeclaration
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
for (const varDecl of varDeclarations) {
|
|
158
|
+
const varName = varDecl.getName();
|
|
159
|
+
const normalizedName = this.normalizeIdentifier(varName);
|
|
160
|
+
|
|
161
|
+
// Check if variable name suggests security usage
|
|
162
|
+
if (!this.isSecurityRelated(normalizedName)) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Skip safe contexts
|
|
167
|
+
if (this.isSafeContext(varName)) {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Get initializer
|
|
172
|
+
const initializer = varDecl.getInitializer();
|
|
173
|
+
if (!initializer) continue;
|
|
174
|
+
|
|
175
|
+
const initText = initializer.getText().toLowerCase();
|
|
176
|
+
|
|
177
|
+
// Skip React components (arrow functions returning JSX)
|
|
178
|
+
if (this.isReactComponent(initText)) {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Check if using unsafe methods
|
|
183
|
+
if (this.isUnsafeGuidGeneration(initText)) {
|
|
184
|
+
// Make sure it's not using safe method
|
|
185
|
+
if (!this.isSafeGuidGeneration(initText)) {
|
|
186
|
+
const line = varDecl.getStartLineNumber();
|
|
187
|
+
|
|
188
|
+
violations.push({
|
|
189
|
+
ruleId: this.ruleId,
|
|
190
|
+
severity: "error",
|
|
191
|
+
message: `Variable '${varName}' uses weak random generation for security purposes - use crypto.randomUUID() or UUID v4 with CSPRNG`,
|
|
192
|
+
line: line,
|
|
193
|
+
column: varDecl.getStart() - varDecl.getStartLinePos() + 1,
|
|
194
|
+
filePath: filePath,
|
|
195
|
+
file: filePath,
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Mark this line as reported to avoid duplicate reports from nested calls
|
|
199
|
+
reportedLines.add(line);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Check call expressions for unsafe random methods
|
|
207
|
+
* e.g., Math.random(), Date.now(), uuidv1()
|
|
208
|
+
*/
|
|
209
|
+
checkCallExpressions(sourceFile, filePath, violations, reportedLines) {
|
|
210
|
+
const callExprs = sourceFile.getDescendantsOfKind(
|
|
211
|
+
SyntaxKind.CallExpression
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
for (const callExpr of callExprs) {
|
|
215
|
+
const line = callExpr.getStartLineNumber();
|
|
216
|
+
|
|
217
|
+
// Skip if already reported at variable level
|
|
218
|
+
if (reportedLines.has(line)) {
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const expression = callExpr.getExpression();
|
|
223
|
+
const expressionText = expression.getText().toLowerCase();
|
|
224
|
+
|
|
225
|
+
// Check if this is an unsafe method call
|
|
226
|
+
if (!this.isUnsafeMethod(expressionText)) {
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Check if this is timestamp arithmetic (time calculation, not generation)
|
|
231
|
+
// e.g., expiry - Date.now(), Date.now() - start, if (time > Date.now())
|
|
232
|
+
if (this.isTimestampArithmetic(callExpr, expressionText)) {
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Check if used in security context
|
|
237
|
+
const parent = this.findParentContext(callExpr);
|
|
238
|
+
if (!parent) continue;
|
|
239
|
+
|
|
240
|
+
const parentText = parent.getText();
|
|
241
|
+
const parentVarName = this.extractVariableName(parent);
|
|
242
|
+
|
|
243
|
+
// Check if parent context is security-related
|
|
244
|
+
if (
|
|
245
|
+
parentVarName &&
|
|
246
|
+
this.isSecurityRelated(this.normalizeIdentifier(parentVarName))
|
|
247
|
+
) {
|
|
248
|
+
// Skip safe contexts
|
|
249
|
+
if (this.isSafeContext(parentVarName)) {
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
violations.push({
|
|
254
|
+
ruleId: this.ruleId,
|
|
255
|
+
severity: "error",
|
|
256
|
+
message: `Unsafe method '${expression.getText()}' used for security-critical GUID generation - use crypto.randomUUID() or UUID v4`,
|
|
257
|
+
line: line,
|
|
258
|
+
column: callExpr.getStart() - callExpr.getStartLinePos() + 1,
|
|
259
|
+
filePath: filePath,
|
|
260
|
+
file: filePath,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// Mark as reported
|
|
264
|
+
reportedLines.add(line);
|
|
265
|
+
continue; // Don't check function context if already reported
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Also check if call expression is in security-related function
|
|
269
|
+
const funcName = this.findParentFunctionName(callExpr);
|
|
270
|
+
if (
|
|
271
|
+
funcName &&
|
|
272
|
+
this.isSecurityRelated(this.normalizeIdentifier(funcName))
|
|
273
|
+
) {
|
|
274
|
+
if (!this.isSafeContext(funcName)) {
|
|
275
|
+
violations.push({
|
|
276
|
+
ruleId: this.ruleId,
|
|
277
|
+
severity: "error",
|
|
278
|
+
message: `Function '${funcName}' uses weak random method '${expression.getText()}' for security purposes - use CSPRNG`,
|
|
279
|
+
line: line,
|
|
280
|
+
column: callExpr.getStart() - callExpr.getStartLinePos() + 1,
|
|
281
|
+
filePath: filePath,
|
|
282
|
+
file: filePath,
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// Mark as reported
|
|
286
|
+
reportedLines.add(line);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Check custom GUID generation functions using Math.random()
|
|
294
|
+
* e.g., function generateGuid() { return 'xxx'.replace(/x/g, () => Math.random()) }
|
|
295
|
+
*/
|
|
296
|
+
checkCustomGuidFunctions(sourceFile, filePath, violations, reportedLines) {
|
|
297
|
+
const functions = [
|
|
298
|
+
...sourceFile.getDescendantsOfKind(SyntaxKind.FunctionDeclaration),
|
|
299
|
+
...sourceFile.getDescendantsOfKind(SyntaxKind.FunctionExpression),
|
|
300
|
+
...sourceFile.getDescendantsOfKind(SyntaxKind.ArrowFunction),
|
|
301
|
+
];
|
|
302
|
+
|
|
303
|
+
for (const func of functions) {
|
|
304
|
+
const line = func.getStartLineNumber();
|
|
305
|
+
|
|
306
|
+
// Skip if already reported
|
|
307
|
+
if (reportedLines.has(line)) {
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const funcName =
|
|
312
|
+
func.getName?.() || this.extractVariableName(func.getParent());
|
|
313
|
+
if (!funcName) continue;
|
|
314
|
+
|
|
315
|
+
const normalizedName = this.normalizeIdentifier(funcName);
|
|
316
|
+
|
|
317
|
+
// Check if function name suggests GUID/UUID generation
|
|
318
|
+
if (
|
|
319
|
+
!normalizedName.includes("guid") &&
|
|
320
|
+
!normalizedName.includes("uuid") &&
|
|
321
|
+
!normalizedName.includes("id") &&
|
|
322
|
+
!normalizedName.includes("token")
|
|
323
|
+
) {
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Skip safe contexts
|
|
328
|
+
if (this.isSafeContext(funcName)) {
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Skip non-generation functions (validation, checking, saving, updating)
|
|
333
|
+
// Only check functions that actually GENERATE GUIDs
|
|
334
|
+
if (this.isNonGenerationFunction(normalizedName)) {
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Check if function body uses weak random
|
|
339
|
+
const funcText = func.getText().toLowerCase();
|
|
340
|
+
|
|
341
|
+
// Skip React components
|
|
342
|
+
if (this.isReactComponent(funcText)) {
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (this.isUnsafeGuidGeneration(funcText)) {
|
|
347
|
+
// Make sure it's not using safe method
|
|
348
|
+
if (!this.isSafeGuidGeneration(funcText)) {
|
|
349
|
+
violations.push({
|
|
350
|
+
ruleId: this.ruleId,
|
|
351
|
+
severity: "error",
|
|
352
|
+
message: `Function '${funcName}' implements weak GUID generation using Math.random() - use crypto.randomUUID() or UUID v4`,
|
|
353
|
+
line: line,
|
|
354
|
+
column: func.getStart() - func.getStartLinePos() + 1,
|
|
355
|
+
filePath: filePath,
|
|
356
|
+
file: filePath,
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
// Mark as reported
|
|
360
|
+
reportedLines.add(line);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Helper: Normalize identifier name (lowercase, remove underscores)
|
|
368
|
+
*/
|
|
369
|
+
normalizeIdentifier(name) {
|
|
370
|
+
return name.toLowerCase().replace(/[_-]/g, "");
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Helper: Check if identifier name is security-related
|
|
375
|
+
*/
|
|
376
|
+
isSecurityRelated(normalizedName) {
|
|
377
|
+
return this.securityKeywords.some((keyword) =>
|
|
378
|
+
normalizedName.includes(keyword)
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Helper: Check if context is safe (non-security usage)
|
|
384
|
+
*/
|
|
385
|
+
isSafeContext(name) {
|
|
386
|
+
const normalized = this.normalizeIdentifier(name);
|
|
387
|
+
return this.safeContexts.some((ctx) => normalized.includes(ctx));
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Helper: Check if text contains unsafe GUID generation
|
|
392
|
+
*/
|
|
393
|
+
isUnsafeGuidGeneration(text) {
|
|
394
|
+
// Skip if it's just timestamp utility functions (business logic, not random)
|
|
395
|
+
if (this.isTimeUtilityFunction(text)) {
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
return this.unsafeMethods.some((method) => text.includes(method));
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Helper: Check if text is timestamp utility function (not random generation)
|
|
403
|
+
*/
|
|
404
|
+
isTimeUtilityFunction(text) {
|
|
405
|
+
return this.timeUtilityPatterns.some((pattern) => text.includes(pattern));
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Helper: Check if text contains safe GUID generation
|
|
410
|
+
*/
|
|
411
|
+
isSafeGuidGeneration(text) {
|
|
412
|
+
return this.safeMethods.some((method) => text.includes(method));
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Helper: Check if function is non-generation (validate, check, save, update)
|
|
417
|
+
*/
|
|
418
|
+
isNonGenerationFunction(normalizedName) {
|
|
419
|
+
const nonGenPatterns = [
|
|
420
|
+
"validate",
|
|
421
|
+
"check",
|
|
422
|
+
"verify",
|
|
423
|
+
"isvalid",
|
|
424
|
+
"save",
|
|
425
|
+
"update",
|
|
426
|
+
"set",
|
|
427
|
+
"get",
|
|
428
|
+
"fetch",
|
|
429
|
+
"load",
|
|
430
|
+
"find",
|
|
431
|
+
"search",
|
|
432
|
+
"query",
|
|
433
|
+
"delete",
|
|
434
|
+
"remove",
|
|
435
|
+
];
|
|
436
|
+
return nonGenPatterns.some((pattern) => normalizedName.includes(pattern));
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Helper: Check if method call is unsafe
|
|
441
|
+
*/
|
|
442
|
+
isUnsafeMethod(methodText) {
|
|
443
|
+
return this.unsafeMethods.some((method) => methodText.includes(method));
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Helper: Check if code is React component (arrow function with props/JSX)
|
|
448
|
+
*/
|
|
449
|
+
isReactComponent(text) {
|
|
450
|
+
// React component patterns:
|
|
451
|
+
// - Arrow function with props parameter
|
|
452
|
+
// - Contains JSX (return <, return null, return (, useEffect, useState, etc.)
|
|
453
|
+
const reactPatterns = [
|
|
454
|
+
"useeffect",
|
|
455
|
+
"usestate",
|
|
456
|
+
"usecontext",
|
|
457
|
+
"usememo",
|
|
458
|
+
"usecallback",
|
|
459
|
+
"useref",
|
|
460
|
+
"return null",
|
|
461
|
+
"return <",
|
|
462
|
+
"return (",
|
|
463
|
+
"props:",
|
|
464
|
+
"props.",
|
|
465
|
+
];
|
|
466
|
+
|
|
467
|
+
return reactPatterns.some((pattern) => text.includes(pattern));
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Helper: Check if Date.now()/timestamp is used in arithmetic operation (time calculation)
|
|
472
|
+
* e.g., expiry - Date.now(), Date.now() - start, time > Date.now()
|
|
473
|
+
*/
|
|
474
|
+
isTimestampArithmetic(callExpr, expressionText) {
|
|
475
|
+
// Only check for Date.now() and timestamp methods
|
|
476
|
+
if (
|
|
477
|
+
!expressionText.includes("date.now") &&
|
|
478
|
+
!expressionText.includes("gettime")
|
|
479
|
+
) {
|
|
480
|
+
return false;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Check if parent is binary expression (arithmetic or comparison)
|
|
484
|
+
let parent = callExpr.getParent();
|
|
485
|
+
let depth = 0;
|
|
486
|
+
|
|
487
|
+
while (parent && depth < 3) {
|
|
488
|
+
const kind = parent.getKind();
|
|
489
|
+
|
|
490
|
+
// Check for binary expressions: +, -, *, /, %, <, >, <=, >=, ==, !=
|
|
491
|
+
if (kind === SyntaxKind.BinaryExpression) {
|
|
492
|
+
const binaryExpr = parent;
|
|
493
|
+
const operator = binaryExpr.getOperatorToken().getText();
|
|
494
|
+
|
|
495
|
+
// Arithmetic operators or comparison operators
|
|
496
|
+
if (
|
|
497
|
+
[
|
|
498
|
+
"-",
|
|
499
|
+
"+",
|
|
500
|
+
"*",
|
|
501
|
+
"/",
|
|
502
|
+
"%",
|
|
503
|
+
"<",
|
|
504
|
+
">",
|
|
505
|
+
"<=",
|
|
506
|
+
">=",
|
|
507
|
+
"==",
|
|
508
|
+
"===",
|
|
509
|
+
"!=",
|
|
510
|
+
"!==",
|
|
511
|
+
].includes(operator)
|
|
512
|
+
) {
|
|
513
|
+
return true; // This is timestamp arithmetic/comparison
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
parent = parent.getParent();
|
|
518
|
+
depth++;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
return false;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Helper: Find parent context (variable declaration, assignment)
|
|
526
|
+
*/
|
|
527
|
+
findParentContext(node) {
|
|
528
|
+
let current = node.getParent();
|
|
529
|
+
let depth = 0;
|
|
530
|
+
|
|
531
|
+
while (current && depth < 10) {
|
|
532
|
+
const kind = current.getKind();
|
|
533
|
+
if (
|
|
534
|
+
kind === SyntaxKind.VariableDeclaration ||
|
|
535
|
+
kind === SyntaxKind.PropertyAssignment ||
|
|
536
|
+
kind === SyntaxKind.BinaryExpression
|
|
537
|
+
) {
|
|
538
|
+
return current;
|
|
539
|
+
}
|
|
540
|
+
current = current.getParent();
|
|
541
|
+
depth++;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Helper: Extract variable name from context
|
|
549
|
+
*/
|
|
550
|
+
extractVariableName(node) {
|
|
551
|
+
if (!node) return null;
|
|
552
|
+
|
|
553
|
+
const kind = node.getKind();
|
|
554
|
+
|
|
555
|
+
if (kind === SyntaxKind.VariableDeclaration) {
|
|
556
|
+
return node.getName?.() || null;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (kind === SyntaxKind.PropertyAssignment) {
|
|
560
|
+
return node.getName?.() || null;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if (kind === SyntaxKind.BinaryExpression) {
|
|
564
|
+
const left = node.getLeft();
|
|
565
|
+
return left.getText();
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
return null;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Helper: Find parent function name
|
|
573
|
+
*/
|
|
574
|
+
findParentFunctionName(node) {
|
|
575
|
+
let current = node.getParent();
|
|
576
|
+
let depth = 0;
|
|
577
|
+
|
|
578
|
+
while (current && depth < 10) {
|
|
579
|
+
const kind = current.getKind();
|
|
580
|
+
if (
|
|
581
|
+
kind === SyntaxKind.FunctionDeclaration ||
|
|
582
|
+
kind === SyntaxKind.FunctionExpression ||
|
|
583
|
+
kind === SyntaxKind.ArrowFunction
|
|
584
|
+
) {
|
|
585
|
+
// For named functions
|
|
586
|
+
const funcName = current.getName?.();
|
|
587
|
+
if (funcName) return funcName;
|
|
588
|
+
|
|
589
|
+
// For arrow functions assigned to const/let/var
|
|
590
|
+
const parent = current.getParent();
|
|
591
|
+
if (parent && parent.getKind() === SyntaxKind.VariableDeclaration) {
|
|
592
|
+
return parent.getName?.() || null;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
return null;
|
|
596
|
+
}
|
|
597
|
+
current = current.getParent();
|
|
598
|
+
depth++;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
return null;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
cleanup() {
|
|
605
|
+
// Cleanup if needed
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
module.exports = S011SymbolBasedAnalyzer;
|