@sun-asterisk/sunlint 1.3.16 → 1.3.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/config/rule-analysis-strategies.js +3 -3
  2. package/config/rules/enhanced-rules-registry.json +40 -20
  3. package/core/cli-action-handler.js +2 -2
  4. package/core/config-merger.js +28 -6
  5. package/core/constants/defaults.js +1 -1
  6. package/core/file-targeting-service.js +72 -4
  7. package/core/output-service.js +21 -4
  8. package/engines/heuristic-engine.js +5 -0
  9. package/package.json +1 -1
  10. package/rules/common/C002_no_duplicate_code/README.md +115 -0
  11. package/rules/common/C002_no_duplicate_code/analyzer.js +615 -219
  12. package/rules/common/C002_no_duplicate_code/test-cases/api-handlers.ts +64 -0
  13. package/rules/common/C002_no_duplicate_code/test-cases/data-processor.ts +46 -0
  14. package/rules/common/C002_no_duplicate_code/test-cases/good-example.tsx +40 -0
  15. package/rules/common/C002_no_duplicate_code/test-cases/product-service.ts +57 -0
  16. package/rules/common/C002_no_duplicate_code/test-cases/user-service.ts +49 -0
  17. package/rules/common/C008/analyzer.js +40 -0
  18. package/rules/common/C008/config.json +20 -0
  19. package/rules/common/C008/ts-morph-analyzer.js +1067 -0
  20. package/rules/common/C018_no_throw_generic_error/analyzer.js +1 -1
  21. package/rules/common/C018_no_throw_generic_error/symbol-based-analyzer.js +27 -3
  22. package/rules/common/C024_no_scatter_hardcoded_constants/symbol-based-analyzer.js +504 -162
  23. package/rules/common/C029_catch_block_logging/analyzer.js +499 -89
  24. package/rules/common/C033_separate_service_repository/README.md +131 -20
  25. package/rules/common/C033_separate_service_repository/analyzer.js +1 -1
  26. package/rules/common/C033_separate_service_repository/symbol-based-analyzer.js +417 -274
  27. package/rules/common/C041_no_sensitive_hardcode/analyzer.js +144 -254
  28. package/rules/common/C041_no_sensitive_hardcode/config.json +50 -0
  29. package/rules/common/C041_no_sensitive_hardcode/symbol-based-analyzer.js +575 -0
  30. package/rules/common/C067_no_hardcoded_config/analyzer.js +17 -16
  31. package/rules/common/C067_no_hardcoded_config/symbol-based-analyzer.js +3477 -659
  32. package/rules/docs/C002_no_duplicate_code.md +276 -11
  33. package/rules/index.js +5 -1
  34. package/rules/security/S006_no_plaintext_recovery_codes/analyzer.js +266 -88
  35. package/rules/security/S006_no_plaintext_recovery_codes/symbol-based-analyzer.js +805 -0
  36. package/rules/security/S010_no_insecure_encryption/README.md +78 -0
  37. package/rules/security/S010_no_insecure_encryption/analyzer.js +463 -398
  38. package/rules/security/S013_tls_enforcement/README.md +51 -0
  39. package/rules/security/S013_tls_enforcement/analyzer.js +99 -0
  40. package/rules/security/S013_tls_enforcement/config.json +41 -0
  41. package/rules/security/S013_tls_enforcement/symbol-based-analyzer.js +339 -0
  42. package/rules/security/S014_tls_version_enforcement/README.md +354 -0
  43. package/rules/security/S014_tls_version_enforcement/analyzer.js +118 -0
  44. package/rules/security/S014_tls_version_enforcement/config.json +56 -0
  45. package/rules/security/S014_tls_version_enforcement/symbol-based-analyzer.js +194 -0
  46. package/rules/security/S055_content_type_validation/analyzer.js +121 -279
  47. package/rules/security/S055_content_type_validation/symbol-based-analyzer.js +346 -0
  48. package/rules/tests/C002_no_duplicate_code.test.js +111 -22
  49. package/rules/common/C029_catch_block_logging/analyzer-smart-pipeline.js +0 -755
  50. package/rules/common/C041_no_sensitive_hardcode/ast-analyzer.js +0 -296
@@ -1,492 +1,557 @@
1
1
  /**
2
- * Heuristic analyzer for S010 - Must use cryptographically secure random number generators (CSPRNG)
3
- * Purpose: Detect usage of insecure random number generators for security purposes
4
- * Based on OWASP A02:2021 - Cryptographic Failures
2
+ * S010 Analyzer using ts-morph for accurate AST-based analysis
3
+ * Detects insecure random usage in REAL security contexts only
4
+ *
5
+ * TRUE SECURITY CONTEXTS:
6
+ * - OTP generation
7
+ * - Token generation (session, reset, verify, magic link)
8
+ * - Password/API key generation
9
+ * - Security code generation
10
+ *
11
+ * NON-SECURITY (should NOT flag):
12
+ * - Filenames with timestamps
13
+ * - Log IDs
14
+ * - Request IDs (tracing)
15
+ * - Expiration time calculations
5
16
  */
6
17
 
18
+ const { Project, SyntaxKind } = require('ts-morph');
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+
7
22
  class S010Analyzer {
8
23
  constructor() {
9
24
  this.ruleId = 'S010';
10
- this.ruleName = 'Must use cryptographically secure random number generators (CSPRNG)';
11
- this.description = 'Detect usage of insecure random number generators for security purposes';
12
-
13
- // Insecure random functions that should not be used for security
14
- this.insecureRandomFunctions = [
15
- // JavaScript/Node.js insecure random functions
16
- 'Math.random',
17
- 'Math.floor(Math.random',
18
- 'Math.ceil(Math.random',
19
- 'Math.round(Math.random',
20
-
21
- // Common insecure patterns
22
- 'new Date().getTime()',
23
- 'Date.now()',
24
- 'performance.now()',
25
- 'process.hrtime()',
26
-
27
- // Insecure libraries
28
- 'random-js',
29
- 'mersenne-twister',
30
- 'seedrandom',
31
-
32
- // Browser APIs (when used for security)
33
- 'window.crypto.getRandomValues', // Actually secure, but context matters
34
- ];
35
-
36
- // Secure random functions (CSPRNG)
37
- this.secureRandomFunctions = [
38
- 'crypto.randomBytes',
39
- 'crypto.randomUUID',
40
- 'crypto.randomInt',
41
- 'crypto.webcrypto.getRandomValues',
42
- 'window.crypto.getRandomValues',
43
- 'require("crypto").randomBytes',
44
- 'import("crypto").randomBytes',
45
- 'webcrypto.getRandomValues',
46
- 'sodium.randombytes_buf',
47
- 'forge.random.getBytesSync',
48
- 'nanoid',
49
- 'uuid.v4',
50
- 'uuidv4',
51
- ];
52
-
53
- // Security-related contexts where secure random is required
54
- this.securityContextKeywords = [
55
- // Authentication
56
- 'password', 'token', 'jwt', 'session', 'auth', 'login', 'signin',
57
- 'activation', 'verification', 'reset', 'recovery', 'otp', 'totp',
25
+ this.MIN_ENTROPY_BITS = 128;
26
+ this.MIN_ENTROPY_BYTES = 16;
27
+
28
+ // TRUE security keywords - only these contexts should trigger
29
+ this.securityKeywords = [
30
+ // Authentication & Authorization
31
+ 'otp', 'totp', 'hotp',
32
+ 'token', 'accesstoken', 'refreshtoken', 'authtoken',
33
+ 'session', 'sessionid', 'sessid',
58
34
 
59
- // Cryptography
60
- 'encrypt', 'decrypt', 'cipher', 'hash', 'salt', 'key', 'secret',
61
- 'nonce', 'iv', 'seed', 'entropy', 'random', 'secure',
35
+ // Password & Keys
36
+ 'password', 'passwd', 'pwd',
37
+ 'apikey', 'secretkey', 'privatekey',
38
+ 'secret', // Generic secret
39
+ 'salt', 'pepper',
62
40
 
63
- // Security tokens
64
- 'csrf', 'xsrf', 'api_key', 'access_token', 'refresh_token',
65
- 'bearer', 'authorization', 'signature', 'certificate',
41
+ // Verification & Reset
42
+ 'verify', 'verification', 'verifycode',
43
+ 'reset', 'resettoken', 'passwordreset',
44
+ 'confirm', 'confirmation', 'confirmcode',
45
+ 'magiclink',
66
46
 
67
- // Identifiers
68
- 'id', 'uuid', 'guid', 'code', 'pin', 'challenge',
47
+ // Security codes
48
+ 'securitycode', 'authcode', 'verificationcode',
49
+ 'pincode', 'pin',
69
50
 
70
- // File/data security
71
- 'upload', 'filename', 'path', 'temp', 'cache'
51
+ // Encryption
52
+ 'encrypt', 'cipher', 'iv', 'nonce',
53
+ 'hmac', 'signature',
72
54
  ];
73
55
 
74
- // Patterns that indicate insecure random usage
75
- this.insecurePatterns = [
76
- // Math.random() variations
77
- /Math\.random\(\)/g,
78
- /Math\.floor\s*\(\s*Math\.random\s*\(\s*\)\s*\*\s*\d+\s*\)/g,
79
- /Math\.ceil\s*\(\s*Math\.random\s*\(\s*\)\s*\*\s*\d+\s*\)/g,
80
- /Math\.round\s*\(\s*Math\.random\s*\(\s*\)\s*\*\s*\d+\s*\)/g,
81
-
82
- // Date-based random (only when used for randomness, not timestamps)
83
- /new\s+Date\(\)\.getTime\(\)/g,
84
- /Date\.now\(\)/g,
85
- /performance\.now\(\)/g,
86
-
87
- // String-based insecure random
88
- /Math\.random\(\)\.toString\(\d*\)\.substring\(\d+\)/g,
89
- /Math\.random\(\)\.toString\(\d*\)\.slice\(\d+\)/g,
90
-
91
- // Simple increment patterns (only in security contexts)
92
- /\+\+\s*\w+|--\s*\w+|\w+\s*\+\+|\w+\s*--/g,
56
+ // EXCLUDE - these are NOT security contexts
57
+ this.nonSecurityKeywords = [
58
+ 'filename', 'file', 'path', 'filepath',
59
+ 'log', 'logging', 'trace', 'debug',
60
+ 'request', 'requestid', 'traceid', 'correlationid',
61
+ 'uuid', 'guid', // UUIDs are OK if properly generated
62
+ 'timestamp', 'time', 'date',
63
+ 'expire', 'expiration', 'ttl', 'timeout',
64
+ 'zip', 'archive', 'backup',
65
+ 'customer', 'user', 'temp', 'tmp',
93
66
  ];
94
67
 
95
- // Patterns that should be excluded (safe contexts)
96
- this.safePatterns = [
97
- // Comments and documentation
98
- /\/\/|\/\*|\*\/|@param|@return|@example/,
99
-
100
- // Type definitions and interfaces
101
- /interface|type|enum|class.*\{/i,
102
-
103
- // Import/export statements
104
- /import|export|require|module\.exports/i,
105
-
106
- // Test files and demo code
107
- /test|spec|demo|example|mock|fixture/i,
108
-
109
- // Non-security contexts
110
- /animation|ui|display|visual|game|chart|graph|color|theme/i,
111
-
112
- // Configuration and constants
113
- /const\s+\w+\s*=|enum\s+\w+|type\s+\w+/i,
114
-
115
- // Safe usage patterns - UI/Animation/Game contexts
116
- /Math\.random\(\).*(?:animation|ui|display|game|demo|test|chart|color|hue)/i,
117
- /(?:animation|ui|display|game|demo|test|chart|color|hue).*Math\.random\(\)/i,
118
-
119
- // Safe class/function contexts
120
- /class\s+(?:UI|Game|Chart|Mock|Demo|Animation)/i,
121
- /function\s+(?:get|generate|create).*(?:Color|Animation|Chart|Game|Mock|Demo)/i,
122
-
123
- // Safe variable names
124
- /(?:const|let|var)\s+(?:color|hue|delay|position|chart|game|mock|demo|animation)/i,
68
+ // Security function names - if variable is inside these functions, it's security context
69
+ this.securityFunctionNames = [
70
+ 'generateotp', 'createotp', 'sendotp',
71
+ 'generatetoken', 'createtoken', 'issuetoken',
72
+ 'generatesession', 'createsession',
73
+ 'generateapikey', 'createapikey',
74
+ 'generatepassword', 'resetpassword', 'changepassword',
75
+ 'generateverificationcode', 'createverificationcode',
76
+ 'generatemagiclink', 'createmagiclink',
77
+ 'generateresettoken', 'createresettoken',
78
+ 'generatesecret', 'createsecret',
79
+ 'encrypt', 'hash', 'sign',
125
80
  ];
126
81
 
127
- // Function patterns that indicate security context
128
- this.securityFunctionPatterns = [
129
- /generate.*(?:token|key|id|code|password|salt|nonce|iv)/i,
130
- /create.*(?:token|key|id|code|password|salt|nonce|iv)/i,
131
- /make.*(?:token|key|id|code|password|salt|nonce|iv)/i,
132
- /random.*(?:token|key|id|code|password|salt|nonce|iv)/i,
133
- /(?:token|key|id|code|password|salt|nonce|iv).*generator/i,
134
- ];
82
+ // Insecure patterns
83
+ this.insecurePatterns = {
84
+ mathRandom: ['Math.random'],
85
+ dateNow: ['Date.now', 'getTime'],
86
+ pythonRandom: ['random.random', 'random.randint', 'random.choice'],
87
+ javaRandom: ['new Random', 'Random.next'],
88
+ phpRandom: ['rand', 'mt_rand', 'uniqid'],
89
+ };
135
90
  }
136
-
91
+
137
92
  async analyze(files, language, options = {}) {
138
93
  const violations = [];
139
94
 
140
95
  for (const filePath of files) {
141
- // Skip test files, build directories, and node_modules
142
- if (this.shouldSkipFile(filePath)) {
143
- continue;
144
- }
145
-
146
96
  try {
147
- const content = require('fs').readFileSync(filePath, 'utf8');
148
- const fileViolations = this.analyzeFile(content, filePath, options);
97
+ const fileViolations = await this.analyzeFile(filePath);
149
98
  violations.push(...fileViolations);
150
99
  } catch (error) {
151
100
  if (options.verbose) {
152
- console.warn(`⚠️ Failed to analyze ${filePath}: ${error.message}`);
101
+ console.error(`Error analyzing ${filePath}:`, error.message);
153
102
  }
154
103
  }
155
104
  }
156
105
 
157
106
  return violations;
158
107
  }
159
-
160
- shouldSkipFile(filePath) {
161
- const skipPatterns = [
162
- 'test/', 'tests/', '__tests__/', '.test.', '.spec.',
163
- 'node_modules/', 'build/', 'dist/', '.next/', 'coverage/',
164
- 'vendor/', 'mocks/', '.mock.'
165
- // Removed 'fixtures/' to allow testing
166
- ];
167
-
168
- return skipPatterns.some(pattern => filePath.includes(pattern));
169
- }
170
-
171
- analyzeFile(content, filePath, options = {}) {
108
+
109
+ async analyzeFile(filePath) {
172
110
  const violations = [];
173
- const lines = content.split('\n');
111
+ const ext = path.extname(filePath);
174
112
 
175
- lines.forEach((line, index) => {
176
- const lineNumber = index + 1;
177
- const trimmedLine = line.trim();
178
-
179
- // Skip comments, imports, and empty lines
180
- if (this.shouldSkipLine(trimmedLine)) {
181
- return;
182
- }
183
-
184
- // Check for insecure random usage in security context
185
- const violation = this.checkForInsecureRandom(line, lineNumber, filePath, content);
186
- if (violation) {
187
- violations.push(violation);
188
- }
113
+ // Only analyze JS/TS files with ts-morph
114
+ if (!['.js', '.jsx', '.ts', '.tsx'].includes(ext)) {
115
+ return violations;
116
+ }
117
+
118
+ const project = new Project();
119
+ const sourceFile = project.addSourceFileAtPath(filePath);
120
+
121
+ // Build function definition map for tracing
122
+ this.functionMap = this.buildFunctionMap(sourceFile);
123
+
124
+ // Find ALL variable declarations (including nested ones inside functions)
125
+ const allVarDecls = sourceFile.getDescendantsOfKind(SyntaxKind.VariableDeclaration);
126
+
127
+ allVarDecls.forEach(varDecl => {
128
+ const violation = this.checkVariableDeclaration(varDecl, filePath, sourceFile);
129
+ if (violation) violations.push(violation);
130
+ });
131
+
132
+ // Find all call expressions (function calls)
133
+ sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression).forEach(call => {
134
+ const violation = this.checkCallExpression(call, filePath);
135
+ if (violation) violations.push(violation);
189
136
  });
190
137
 
191
138
  return violations;
192
139
  }
193
-
194
- shouldSkipLine(line) {
195
- // Skip comments, imports, and other non-code lines
196
- return (
197
- line.length === 0 ||
198
- line.startsWith('//') ||
199
- line.startsWith('/*') ||
200
- line.startsWith('*') ||
201
- line.startsWith('import ') ||
202
- line.startsWith('export ') ||
203
- line.startsWith('require(') ||
204
- line.includes('module.exports')
205
- );
206
- }
207
-
208
- checkForInsecureRandom(line, lineNumber, filePath, fullContent) {
209
- const lowerLine = line.toLowerCase();
140
+
141
+ /**
142
+ * Build a map of function definitions to trace helper functions
143
+ * Includes imported functions from other files
144
+ */
145
+ buildFunctionMap(sourceFile) {
146
+ const functionMap = new Map();
147
+
148
+ // Get all function declarations in current file
149
+ sourceFile.getFunctions().forEach(func => {
150
+ const name = func.getName();
151
+ if (name) {
152
+ functionMap.set(name.toLowerCase(), func);
153
+ }
154
+ });
210
155
 
211
- // First check if line contains safe patterns (early exit)
212
- if (this.containsSafePattern(line)) {
213
- return null;
214
- }
156
+ // Get arrow functions assigned to variables
157
+ sourceFile.getVariableDeclarations().forEach(varDecl => {
158
+ const initializer = varDecl.getInitializer();
159
+ if (initializer &&
160
+ (initializer.getKind() === SyntaxKind.ArrowFunction ||
161
+ initializer.getKind() === SyntaxKind.FunctionExpression)) {
162
+ const name = varDecl.getName();
163
+ if (name) {
164
+ functionMap.set(name.toLowerCase(), initializer);
165
+ }
166
+ }
167
+ });
215
168
 
216
- // Check for insecure random patterns
217
- for (const pattern of this.insecurePatterns) {
218
- const matches = [...line.matchAll(pattern)];
169
+ // Resolve imported functions from other files
170
+ this.resolveImportedFunctions(sourceFile, functionMap);
171
+
172
+ return functionMap;
173
+ }
174
+
175
+ /**
176
+ * Resolve imported functions from require() or import statements
177
+ */
178
+ resolveImportedFunctions(sourceFile, functionMap) {
179
+ const filePath = sourceFile.getFilePath();
180
+ const fileDir = path.dirname(filePath);
181
+
182
+ // Find all require/import statements
183
+ sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression).forEach(callExpr => {
184
+ const expr = callExpr.getExpression();
219
185
 
220
- for (const match of matches) {
221
- // Special handling for Date.now() - check if it's legitimate timestamp usage
222
- if (match[0].includes('Date.now()') && this.isLegitimateTimestampUsage(line)) {
223
- continue; // Skip this match
224
- }
225
-
226
- // Check if this usage is in a security context
227
- if (this.isInSecurityContext(line, fullContent, lineNumber)) {
228
- const column = match.index + 1;
186
+ // Check if it's require('...')
187
+ if (expr.getText() === 'require') {
188
+ const args = callExpr.getArguments();
189
+ if (args.length > 0) {
190
+ const importPath = args[0].getText().replace(/['"]/g, '');
229
191
 
230
- return {
231
- ruleId: this.ruleId,
232
- severity: 'error',
233
- message: 'Must use cryptographically secure random number generators (CSPRNG) for security purposes. Math.random() and similar functions are not secure.',
234
- line: lineNumber,
235
- column: column,
236
- filePath: filePath,
237
- type: 'insecure_random_usage',
238
- details: this.getSecureAlternatives(match[0]),
239
- insecureFunction: match[0]
240
- };
192
+ // Only resolve relative imports
193
+ if (importPath.startsWith('./') || importPath.startsWith('../')) {
194
+ try {
195
+ const resolvedPath = this.resolveImportPath(importPath, fileDir);
196
+
197
+ if (fs.existsSync(resolvedPath)) {
198
+ const importedFunctions = this.extractFunctionsFromFile(resolvedPath);
199
+
200
+ // Get imported names
201
+ const parent = callExpr.getParent();
202
+ if (parent.getKind() === SyntaxKind.VariableDeclaration) {
203
+ const varName = parent.getFirstChildByKind(SyntaxKind.Identifier)?.getText();
204
+ if (varName && importedFunctions.has(varName.toLowerCase())) {
205
+ functionMap.set(varName.toLowerCase(), importedFunctions.get(varName.toLowerCase()));
206
+ }
207
+
208
+ // Handle destructuring: const { randomString } = require(...)
209
+ const bindingPattern = parent.getFirstChildByKind(SyntaxKind.ObjectBindingPattern);
210
+ if (bindingPattern) {
211
+ bindingPattern.getElements().forEach(element => {
212
+ const name = element.getName();
213
+ if (name && importedFunctions.has(name.toLowerCase())) {
214
+ functionMap.set(name.toLowerCase(), importedFunctions.get(name.toLowerCase()));
215
+ }
216
+ });
217
+ }
218
+ }
219
+ }
220
+ } catch (error) {
221
+ // Silently skip import resolution errors
222
+ }
223
+ }
241
224
  }
242
225
  }
226
+ });
227
+ }
228
+
229
+ /**
230
+ * Resolve import path to absolute file path
231
+ */
232
+ resolveImportPath(importPath, baseDir) {
233
+ let resolved = path.resolve(baseDir, importPath);
234
+
235
+ // Try with .js extension
236
+ if (!fs.existsSync(resolved)) {
237
+ resolved = resolved + '.js';
243
238
  }
244
239
 
245
- // Check for insecure function calls
246
- const insecureFunctionViolation = this.checkInsecureFunctionCall(line, lineNumber, filePath, fullContent);
247
- if (insecureFunctionViolation) {
248
- return insecureFunctionViolation;
240
+ // Try with .ts extension
241
+ if (!fs.existsSync(resolved)) {
242
+ resolved = resolved.replace(/\.js$/, '.ts');
249
243
  }
250
244
 
251
- return null;
252
- }
253
-
254
- containsSafePattern(line) {
255
- return this.safePatterns.some(pattern => pattern.test(line));
245
+ return resolved;
256
246
  }
257
-
258
- isInSecurityContext(line, fullContent, lineNumber) {
259
- const lowerLine = line.toLowerCase();
260
- const lowerContent = fullContent.toLowerCase();
261
-
262
- // First check if this is explicitly a non-security context
263
- if (this.isNonSecurityContext(line, fullContent, lineNumber)) {
264
- return false;
265
- }
266
-
267
- // Check if line contains security keywords
268
- const hasSecurityKeyword = this.securityContextKeywords.some(keyword =>
269
- lowerLine.includes(keyword)
270
- );
271
-
272
- if (hasSecurityKeyword) {
273
- return true;
247
+
248
+ /**
249
+ * Extract function definitions from an external file
250
+ */
251
+ extractFunctionsFromFile(filePath) {
252
+ const functionMap = new Map();
253
+
254
+ try {
255
+ const project = new Project();
256
+ const sourceFile = project.addSourceFileAtPath(filePath);
257
+
258
+ // Get exported functions
259
+ sourceFile.getFunctions().forEach(func => {
260
+ const name = func.getName();
261
+ if (name) {
262
+ functionMap.set(name.toLowerCase(), func);
263
+ }
264
+ });
265
+
266
+ // Get exported arrow functions
267
+ sourceFile.getVariableDeclarations().forEach(varDecl => {
268
+ const initializer = varDecl.getInitializer();
269
+ if (initializer &&
270
+ (initializer.getKind() === SyntaxKind.ArrowFunction ||
271
+ initializer.getKind() === SyntaxKind.FunctionExpression)) {
272
+ const name = varDecl.getName();
273
+ if (name) {
274
+ functionMap.set(name.toLowerCase(), initializer);
275
+ }
276
+ }
277
+ });
278
+ } catch (error) {
279
+ // Silently skip errors
274
280
  }
275
281
 
276
- // Check function context (look at function name)
277
- const functionContext = this.getFunctionContext(fullContent, lineNumber);
278
- if (functionContext && this.isSecurityFunction(functionContext)) {
279
- return true;
280
- }
282
+ return functionMap;
283
+ }
284
+
285
+ /**
286
+ * Check if a function contains insecure random usage
287
+ */
288
+ functionContainsInsecureRandom(funcNode) {
289
+ if (!funcNode) return false;
290
+
291
+ const funcText = funcNode.getText();
292
+ return this.hasInsecureRandomUsage(funcText);
293
+ }
294
+
295
+ checkVariableDeclaration(varDecl, filePath, sourceFile) {
296
+ const varName = varDecl.getName().toLowerCase();
297
+ const initializer = varDecl.getInitializer();
281
298
 
282
- // Check variable context
283
- const variableContext = this.getVariableContext(line);
284
- if (variableContext && this.isSecurityVariable(variableContext)) {
285
- return true;
286
- }
299
+ if (!initializer) return null;
287
300
 
288
- // Check surrounding lines for context
289
- const contextLines = this.getSurroundingLines(fullContent, lineNumber, 3);
290
- const contextHasSecurityKeywords = this.securityContextKeywords.some(keyword =>
291
- contextLines.some(contextLine => contextLine.toLowerCase().includes(keyword))
292
- );
301
+ const lineNum = varDecl.getStartLineNumber();
293
302
 
294
- return contextHasSecurityKeywords;
295
- }
296
-
297
- isNonSecurityContext(line, fullContent, lineNumber) {
298
- const lowerLine = line.toLowerCase();
299
-
300
- // Check for UI/Game/Animation contexts
301
- const nonSecurityKeywords = [
302
- 'animation', 'ui', 'display', 'visual', 'game', 'chart', 'graph',
303
- 'color', 'theme', 'hue', 'rgb', 'hsl', 'position', 'coordinate',
304
- 'mock', 'demo', 'test', 'example', 'sample', 'fixture'
305
- ];
303
+ // Check if variable name indicates security context
304
+ const isSecurityVar = this.isSecurityVariableName(varName);
305
+ const isNonSecurityVar = this.isNonSecurityVariableName(varName);
306
306
 
307
- if (nonSecurityKeywords.some(keyword => lowerLine.includes(keyword))) {
308
- return true;
307
+ // If explicitly non-security, skip
308
+ if (isNonSecurityVar) {
309
+ return null;
309
310
  }
310
311
 
311
- // Check class context
312
- const classContext = this.getClassContext(fullContent, lineNumber);
313
- if (classContext) {
314
- const lowerClassName = classContext.toLowerCase();
315
- if (nonSecurityKeywords.some(keyword => lowerClassName.includes(keyword))) {
316
- return true;
317
- }
318
- }
312
+ // Check if initializer uses insecure random directly
313
+ let hasInsecureRandom = this.hasInsecureRandomUsage(initializer.getText());
314
+ let traceInfo = null;
319
315
 
320
- // Check function context
321
- const functionContext = this.getFunctionContext(fullContent, lineNumber);
322
- if (functionContext) {
323
- const lowerFunctionName = functionContext.toLowerCase();
324
- if (nonSecurityKeywords.some(keyword => lowerFunctionName.includes(keyword))) {
325
- return true;
316
+ // If no direct insecure random but security context, trace function calls
317
+ if (!hasInsecureRandom && isSecurityVar && initializer.getKind() === SyntaxKind.CallExpression) {
318
+ const callExpr = initializer;
319
+ const functionName = callExpr.getExpression().getText();
320
+
321
+ // Check if this function contains insecure random
322
+ const funcDef = this.functionMap?.get(functionName.toLowerCase());
323
+ if (funcDef && this.functionContainsInsecureRandom(funcDef)) {
324
+ hasInsecureRandom = true;
325
+ traceInfo = {
326
+ helperFunction: functionName,
327
+ message: `calls helper function "${functionName}()" which uses insecure random`
328
+ };
326
329
  }
327
330
  }
328
331
 
329
- return false;
330
- }
331
-
332
- getClassContext(content, lineNumber) {
333
- const lines = content.split('\n');
334
-
335
- // Look backwards for class declaration
336
- for (let i = lineNumber - 1; i >= Math.max(0, lineNumber - 20); i--) {
337
- const line = lines[i];
338
- const classMatch = line.match(/class\s+(\w+)/);
339
- if (classMatch) {
340
- return classMatch[1];
341
- }
342
- }
332
+ if (!hasInsecureRandom) return null;
343
333
 
344
- return null;
345
- }
346
-
347
- checkInsecureFunctionCall(line, lineNumber, filePath, fullContent) {
348
- // Look for specific insecure function patterns
349
- const mathRandomMatch = line.match(/(Math\.random\(\))/);
350
- if (mathRandomMatch && this.isInSecurityContext(line, fullContent, lineNumber)) {
334
+ // Check if inside security function context (even if variable name is generic)
335
+ const isInSecurityFunction = this.isInSecurityFunctionContext(varDecl);
336
+
337
+ if (hasInsecureRandom && (isSecurityVar || isInSecurityFunction)) {
338
+ let reason;
339
+
340
+ if (traceInfo) {
341
+ // Traced through helper function
342
+ reason = `Variable "${varDecl.getName()}" ${traceInfo.message}`;
343
+ } else if (isSecurityVar) {
344
+ reason = `Variable "${varDecl.getName()}" uses insecure random for security purpose.`;
345
+ } else {
346
+ reason = `Variable "${varDecl.getName()}" inside security function uses insecure random.`;
347
+ }
348
+
351
349
  return {
352
350
  ruleId: this.ruleId,
353
351
  severity: 'error',
354
- message: 'Math.random() is not cryptographically secure. Use crypto.randomBytes() or crypto.randomInt() for security purposes.',
355
- line: lineNumber,
356
- column: mathRandomMatch.index + 1,
352
+ message: `${reason} Use crypto.randomBytes() or crypto.randomUUID().`,
353
+ line: varDecl.getStartLineNumber(),
354
+ column: varDecl.getStart(),
357
355
  filePath: filePath,
358
- type: 'math_random_insecure',
359
- details: 'Consider using: crypto.randomBytes(), crypto.randomInt(), crypto.randomUUID(), or nanoid() for secure random generation.',
360
- insecureFunction: mathRandomMatch[1]
356
+ context: varName,
357
+ ...(traceInfo && { helperFunction: traceInfo.helperFunction })
361
358
  };
362
359
  }
363
360
 
364
- // Check for Date-based random, but exclude legitimate timestamp usage
365
- const dateRandomMatch = line.match(/(Date\.now\(\)|new\s+Date\(\)\.getTime\(\))/);
366
- if (dateRandomMatch && this.isInSecurityContext(line, fullContent, lineNumber)) {
367
- // Check if this is legitimate timestamp usage (JWT iat/exp, logging, etc.)
368
- if (this.isLegitimateTimestampUsage(line)) {
369
- return null;
361
+ return null;
362
+ }
363
+
364
+ checkCallExpression(call, filePath) {
365
+ const callText = call.getText();
366
+
367
+ // Check if it's Math.random() or Date.now()
368
+ if (!this.hasInsecureRandomUsage(callText)) {
369
+ return null;
370
+ }
371
+
372
+ // Get the context where this call is used
373
+ const parent = call.getParent();
374
+ const grandParent = parent?.getParent();
375
+
376
+ // Check property assignment: { token: Math.random() }
377
+ if (parent.getKind() === SyntaxKind.PropertyAssignment) {
378
+ const propName = parent.getChildAtIndex(0).getText().toLowerCase();
379
+
380
+ if (this.isSecurityVariableName(propName) && !this.isNonSecurityVariableName(propName)) {
381
+ return {
382
+ ruleId: this.ruleId,
383
+ severity: 'error',
384
+ message: `Property "${propName}" uses insecure random for security purpose.`,
385
+ line: call.getStartLineNumber(),
386
+ column: call.getStart(),
387
+ filePath: filePath,
388
+ context: propName,
389
+ };
370
390
  }
391
+ }
392
+
393
+ // Check variable declaration
394
+ if (grandParent?.getKind() === SyntaxKind.VariableDeclaration) {
395
+ const varName = grandParent.getChildAtIndex(0).getText().toLowerCase();
371
396
 
372
- return {
373
- ruleId: this.ruleId,
374
- severity: 'warning',
375
- message: 'Using timestamp for random generation is predictable and insecure.',
376
- line: lineNumber,
377
- column: dateRandomMatch.index + 1,
378
- filePath: filePath,
379
- type: 'timestamp_random_insecure',
380
- details: 'Consider using crypto.randomBytes() or crypto.randomUUID() for secure random generation.',
381
- insecureFunction: dateRandomMatch[1]
382
- };
397
+ if (this.isSecurityVariableName(varName) && !this.isNonSecurityVariableName(varName)) {
398
+ return {
399
+ ruleId: this.ruleId,
400
+ severity: 'error',
401
+ message: `Variable "${varName}" uses insecure random for security purpose.`,
402
+ line: call.getStartLineNumber(),
403
+ column: call.getStart(),
404
+ filePath: filePath,
405
+ context: varName,
406
+ };
407
+ }
383
408
  }
384
409
 
385
410
  return null;
386
411
  }
387
-
388
- isLegitimateTimestampUsage(line) {
389
- // Check for legitimate timestamp usage patterns
390
- const legitimatePatterns = [
391
- // JWT timestamp fields - more flexible matching
392
- /\b(?:iat|exp|nbf)\s*:\s*Math\.floor\s*\(\s*Date\.now\(\)\s*\/\s*1000\s*\)/,
393
- /Math\.floor\s*\(\s*Date\.now\(\)\s*\/\s*1000\s*\).*(?:iat|exp|nbf)/i,
394
-
395
- // JWT timestamp with arithmetic
396
- /Math\.floor\s*\(\s*Date\.now\(\)\s*\/\s*1000\s*\)\s*[\+\-]\s*\d+/,
397
-
398
- // Logging timestamps
399
- /timestamp\s*:\s*Date\.now\(\)/i,
400
- /createdAt\s*:\s*new\s+Date\(\)/i,
401
- /updatedAt\s*:\s*new\s+Date\(\)/i,
402
-
403
- // Expiration times
404
- /expiresAt\s*:\s*new\s+Date\s*\(\s*Date\.now\(\)/i,
405
- /expiry\s*:\s*Date\.now\(\)/i,
406
-
407
- // Performance measurement
408
- /performance\.now\(\)/,
412
+
413
+ isSecurityVariableName(name) {
414
+ const normalized = name.toLowerCase().replace(/[_\-]/g, '');
415
+ return this.securityKeywords.some(keyword =>
416
+ normalized.includes(keyword.toLowerCase())
417
+ );
418
+ }
419
+
420
+ isNonSecurityVariableName(name) {
421
+ const normalized = name.toLowerCase().replace(/[_\-]/g, '');
422
+ return this.nonSecurityKeywords.some(keyword =>
423
+ normalized.includes(keyword.toLowerCase())
424
+ );
425
+ }
426
+
427
+ isInSecurityFunctionContext(node) {
428
+ // Walk up the AST to find parent function
429
+ let current = node.getParent();
430
+
431
+ while (current) {
432
+ const kind = current.getKind();
409
433
 
410
- // Date arithmetic (not for randomness)
411
- /Date\.now\(\)\s*[\+\-]\s*\d+/,
412
- /new\s+Date\s*\(\s*Date\.now\(\)\s*[\+\-]/,
434
+ // Check if it's a function declaration or arrow function
435
+ if (kind === SyntaxKind.FunctionDeclaration ||
436
+ kind === SyntaxKind.ArrowFunction ||
437
+ kind === SyntaxKind.FunctionExpression) {
438
+
439
+ // Get function name
440
+ let funcName = '';
441
+
442
+ if (kind === SyntaxKind.FunctionDeclaration) {
443
+ const nameNode = current.getNameNode();
444
+ funcName = nameNode ? nameNode.getText() : '';
445
+ } else {
446
+ // For arrow functions, check if assigned to a variable
447
+ const parent = current.getParent();
448
+ if (parent && parent.getKind() === SyntaxKind.VariableDeclaration) {
449
+ const nameNode = parent.getNameNode();
450
+ funcName = nameNode ? nameNode.getText() : '';
451
+ }
452
+ }
453
+
454
+ if (funcName) {
455
+ const normalizedFuncName = funcName.toLowerCase().replace(/[_\-]/g, '');
456
+
457
+ // Check if function name matches security patterns
458
+ const isSecurityFunc = this.securityFunctionNames.some(keyword =>
459
+ normalizedFuncName.includes(keyword)
460
+ );
461
+
462
+ if (isSecurityFunc) {
463
+ return true;
464
+ }
465
+ }
466
+ }
413
467
 
414
- // JWT context - check for jwt, token, payload keywords nearby
415
- /(?:jwt|token|payload).*Date\.now\(\)/i,
416
- /Date\.now\(\).*(?:jwt|token|payload)/i,
417
- ];
468
+ current = current.getParent();
469
+ }
418
470
 
419
- return legitimatePatterns.some(pattern => pattern.test(line));
471
+ return false;
420
472
  }
421
-
422
- getFunctionContext(content, lineNumber) {
423
- const lines = content.split('\n');
424
-
425
- // Look backwards for function declaration
426
- for (let i = lineNumber - 1; i >= Math.max(0, lineNumber - 10); i--) {
427
- const line = lines[i];
428
- const functionMatch = line.match(/(?:function\s+(\w+)|(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\w*\s*(?:function\s*)?|\s*(\w+)\s*[:=]\s*(?:async\s+)?(?:function|\w*\s*=>))/);
429
- if (functionMatch) {
430
- return functionMatch[1] || functionMatch[2] || functionMatch[3];
431
- }
473
+
474
+ hasInsecureRandomUsage(text) {
475
+ // Check for Math.random()
476
+ if (/Math\.random\s*\(/.test(text)) {
477
+ return true;
432
478
  }
433
479
 
434
- return null;
435
- }
436
-
437
- isSecurityFunction(functionName) {
438
- if (!functionName) return false;
480
+ // Check for Date.now() or getTime() when used for randomness (not timestamp)
481
+ // Only flag if used with toString(36) or similar encoding
482
+ if (/Date\.now\s*\(\).*\.toString\s*\(/.test(text)) {
483
+ return true;
484
+ }
439
485
 
440
- const lowerFunctionName = functionName.toLowerCase();
441
- return this.securityFunctionPatterns.some(pattern => pattern.test(lowerFunctionName)) ||
442
- this.securityContextKeywords.some(keyword => lowerFunctionName.includes(keyword));
443
- }
444
-
445
- getVariableContext(line) {
446
- // Extract variable name from assignment
447
- const assignmentMatch = line.match(/(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=/);
448
- if (assignmentMatch) {
449
- return assignmentMatch[1];
486
+ if (/getTime\s*\(\).*\.toString\s*\(/.test(text)) {
487
+ return true;
450
488
  }
451
489
 
452
- // Extract property assignment
453
- const propertyMatch = line.match(/(\w+)\s*[:=]/);
454
- if (propertyMatch) {
455
- return propertyMatch[1];
490
+ // Check for timestamp-based patterns with encoding
491
+ // Pattern: btoa(+new Date), btoa(Date.now()), etc.
492
+ if (/btoa\s*\(\s*\+\s*new\s+Date/.test(text)) {
493
+ return true;
456
494
  }
457
495
 
458
- return null;
459
- }
460
-
461
- isSecurityVariable(variableName) {
462
- if (!variableName) return false;
496
+ if (/btoa\s*\(\s*Date\.now/.test(text)) {
497
+ return true;
498
+ }
463
499
 
464
- const lowerVariableName = variableName.toLowerCase();
465
- return this.securityContextKeywords.some(keyword => lowerVariableName.includes(keyword));
466
- }
467
-
468
- getSurroundingLines(content, lineNumber, range) {
469
- const lines = content.split('\n');
470
- const start = Math.max(0, lineNumber - range - 1);
471
- const end = Math.min(lines.length, lineNumber + range);
500
+ // Check for +new Date (unary plus operator on Date)
501
+ // This converts Date to timestamp, similar to Date.now()
502
+ if (/\+\s*new\s+Date.*\.(?:toString|slice|substr|substring)/.test(text)) {
503
+ return true;
504
+ }
472
505
 
473
- return lines.slice(start, end);
506
+ // Check for new Date().getTime() with encoding
507
+ if (/new\s+Date\s*\(\s*\)\.getTime\s*\(\).*\.toString/.test(text)) {
508
+ return true;
509
+ }
510
+
511
+ // Check for Buffer.from() with timestamp or Math.random
512
+ // Pattern: Buffer.from(String(Date.now())).toString('base64')
513
+ if (/Buffer\.from\s*\(.*(?:Date\.now|getTime|\+\s*new\s+Date|Math\.random).*\)\.toString\s*\(/.test(text)) {
514
+ return true;
515
+ }
516
+
517
+ // Check for btoa() with Math.random
518
+ if (/btoa\s*\(.*Math\.random/.test(text)) {
519
+ return true;
520
+ }
521
+
522
+ // Check for performance.now() - High-resolution timestamp (also predictable)
523
+ if (/performance\.now\s*\(\)/.test(text)) {
524
+ return true;
525
+ }
526
+
527
+ // Check for process.pid (low entropy - predictable process ID)
528
+ if (/process\.pid\b/.test(text)) {
529
+ return true;
530
+ }
531
+
532
+ // Check for process.hrtime() - High-resolution time (timestamp-based)
533
+ if (/process\.hrtime(?:\.bigint)?\s*\(/.test(text)) {
534
+ return true;
535
+ }
536
+
537
+ return false;
474
538
  }
475
-
476
- getSecureAlternatives(insecureFunction) {
539
+
540
+ getSecureAlternatives(language = 'javascript') {
477
541
  const alternatives = {
478
- 'Math.random()': 'crypto.randomBytes(), crypto.randomInt(), or crypto.randomUUID()',
479
- 'Date.now()': 'crypto.randomBytes() or crypto.randomUUID()',
480
- 'new Date().getTime()': 'crypto.randomBytes() or crypto.randomUUID()',
481
- 'performance.now()': 'crypto.randomBytes() or crypto.randomUUID()'
542
+ javascript: [
543
+ 'crypto.randomBytes(16).toString("hex")',
544
+ 'crypto.randomUUID()',
545
+ 'crypto.randomInt(100000, 999999) // for OTP',
546
+ ],
547
+ python: [
548
+ 'secrets.token_hex(16)',
549
+ 'secrets.token_urlsafe(16)',
550
+ 'secrets.randbelow(900000) + 100000 # for OTP',
551
+ ],
482
552
  };
483
553
 
484
- return alternatives[insecureFunction] || 'Use crypto.randomBytes(), crypto.randomInt(), or crypto.randomUUID() for secure random generation.';
485
- }
486
-
487
- findPatternColumn(line, pattern) {
488
- const match = pattern.exec(line);
489
- return match ? match.index + 1 : 1;
554
+ return alternatives[language] || alternatives.javascript;
490
555
  }
491
556
  }
492
557