@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.
Files changed (34) hide show
  1. package/config/rules/enhanced-rules-registry.json +77 -18
  2. package/core/cli-program.js +2 -1
  3. package/core/github-annotate-service.js +89 -0
  4. package/core/output-service.js +25 -0
  5. package/core/summary-report-service.js +30 -30
  6. package/package.json +3 -2
  7. package/rules/common/C014_dependency_injection/symbol-based-analyzer.js +392 -280
  8. package/rules/common/C017_constructor_logic/analyzer.js +137 -503
  9. package/rules/common/C017_constructor_logic/config.json +50 -0
  10. package/rules/common/C017_constructor_logic/symbol-based-analyzer.js +463 -0
  11. package/rules/security/S006_no_plaintext_recovery_codes/symbol-based-analyzer.js +463 -21
  12. package/rules/security/S011_secure_guid_generation/README.md +255 -0
  13. package/rules/security/S011_secure_guid_generation/analyzer.js +135 -0
  14. package/rules/security/S011_secure_guid_generation/config.json +56 -0
  15. package/rules/security/S011_secure_guid_generation/symbol-based-analyzer.js +609 -0
  16. package/rules/security/S028_file_upload_size_limits/README.md +537 -0
  17. package/rules/security/S028_file_upload_size_limits/analyzer.js +202 -0
  18. package/rules/security/S028_file_upload_size_limits/config.json +186 -0
  19. package/rules/security/S028_file_upload_size_limits/symbol-based-analyzer.js +530 -0
  20. package/rules/security/S041_session_token_invalidation/README.md +303 -0
  21. package/rules/security/S041_session_token_invalidation/analyzer.js +242 -0
  22. package/rules/security/S041_session_token_invalidation/config.json +175 -0
  23. package/rules/security/S041_session_token_invalidation/regex-based-analyzer.js +411 -0
  24. package/rules/security/S041_session_token_invalidation/symbol-based-analyzer.js +674 -0
  25. package/rules/security/S044_re_authentication_required/README.md +136 -0
  26. package/rules/security/S044_re_authentication_required/analyzer.js +242 -0
  27. package/rules/security/S044_re_authentication_required/config.json +161 -0
  28. package/rules/security/S044_re_authentication_required/regex-based-analyzer.js +329 -0
  29. package/rules/security/S044_re_authentication_required/symbol-based-analyzer.js +537 -0
  30. package/rules/security/S045_brute_force_protection/README.md +345 -0
  31. package/rules/security/S045_brute_force_protection/analyzer.js +336 -0
  32. package/rules/security/S045_brute_force_protection/config.json +139 -0
  33. package/rules/security/S045_brute_force_protection/symbol-based-analyzer.js +646 -0
  34. 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;