@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.
- package/config/rule-analysis-strategies.js +3 -3
- package/config/rules/enhanced-rules-registry.json +40 -20
- package/core/cli-action-handler.js +2 -2
- package/core/config-merger.js +28 -6
- package/core/constants/defaults.js +1 -1
- package/core/file-targeting-service.js +72 -4
- package/core/output-service.js +21 -4
- package/engines/heuristic-engine.js +5 -0
- package/package.json +1 -1
- package/rules/common/C002_no_duplicate_code/README.md +115 -0
- package/rules/common/C002_no_duplicate_code/analyzer.js +615 -219
- package/rules/common/C002_no_duplicate_code/test-cases/api-handlers.ts +64 -0
- package/rules/common/C002_no_duplicate_code/test-cases/data-processor.ts +46 -0
- package/rules/common/C002_no_duplicate_code/test-cases/good-example.tsx +40 -0
- package/rules/common/C002_no_duplicate_code/test-cases/product-service.ts +57 -0
- package/rules/common/C002_no_duplicate_code/test-cases/user-service.ts +49 -0
- package/rules/common/C008/analyzer.js +40 -0
- package/rules/common/C008/config.json +20 -0
- package/rules/common/C008/ts-morph-analyzer.js +1067 -0
- package/rules/common/C018_no_throw_generic_error/analyzer.js +1 -1
- package/rules/common/C018_no_throw_generic_error/symbol-based-analyzer.js +27 -3
- package/rules/common/C024_no_scatter_hardcoded_constants/symbol-based-analyzer.js +504 -162
- package/rules/common/C029_catch_block_logging/analyzer.js +499 -89
- package/rules/common/C033_separate_service_repository/README.md +131 -20
- package/rules/common/C033_separate_service_repository/analyzer.js +1 -1
- package/rules/common/C033_separate_service_repository/symbol-based-analyzer.js +417 -274
- package/rules/common/C041_no_sensitive_hardcode/analyzer.js +144 -254
- package/rules/common/C041_no_sensitive_hardcode/config.json +50 -0
- package/rules/common/C041_no_sensitive_hardcode/symbol-based-analyzer.js +575 -0
- package/rules/common/C067_no_hardcoded_config/analyzer.js +17 -16
- package/rules/common/C067_no_hardcoded_config/symbol-based-analyzer.js +3477 -659
- package/rules/docs/C002_no_duplicate_code.md +276 -11
- package/rules/index.js +5 -1
- package/rules/security/S006_no_plaintext_recovery_codes/analyzer.js +266 -88
- package/rules/security/S006_no_plaintext_recovery_codes/symbol-based-analyzer.js +805 -0
- package/rules/security/S010_no_insecure_encryption/README.md +78 -0
- package/rules/security/S010_no_insecure_encryption/analyzer.js +463 -398
- package/rules/security/S013_tls_enforcement/README.md +51 -0
- package/rules/security/S013_tls_enforcement/analyzer.js +99 -0
- package/rules/security/S013_tls_enforcement/config.json +41 -0
- package/rules/security/S013_tls_enforcement/symbol-based-analyzer.js +339 -0
- package/rules/security/S014_tls_version_enforcement/README.md +354 -0
- package/rules/security/S014_tls_version_enforcement/analyzer.js +118 -0
- package/rules/security/S014_tls_version_enforcement/config.json +56 -0
- package/rules/security/S014_tls_version_enforcement/symbol-based-analyzer.js +194 -0
- package/rules/security/S055_content_type_validation/analyzer.js +121 -279
- package/rules/security/S055_content_type_validation/symbol-based-analyzer.js +346 -0
- package/rules/tests/C002_no_duplicate_code.test.js +111 -22
- package/rules/common/C029_catch_block_logging/analyzer-smart-pipeline.js +0 -755
- package/rules/common/C041_no_sensitive_hardcode/ast-analyzer.js +0 -296
|
@@ -1,492 +1,557 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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.
|
|
11
|
-
this.
|
|
12
|
-
|
|
13
|
-
//
|
|
14
|
-
this.
|
|
15
|
-
//
|
|
16
|
-
'
|
|
17
|
-
'
|
|
18
|
-
'
|
|
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
|
-
//
|
|
60
|
-
'
|
|
61
|
-
'
|
|
35
|
+
// Password & Keys
|
|
36
|
+
'password', 'passwd', 'pwd',
|
|
37
|
+
'apikey', 'secretkey', 'privatekey',
|
|
38
|
+
'secret', // Generic secret
|
|
39
|
+
'salt', 'pepper',
|
|
62
40
|
|
|
63
|
-
//
|
|
64
|
-
'
|
|
65
|
-
'
|
|
41
|
+
// Verification & Reset
|
|
42
|
+
'verify', 'verification', 'verifycode',
|
|
43
|
+
'reset', 'resettoken', 'passwordreset',
|
|
44
|
+
'confirm', 'confirmation', 'confirmcode',
|
|
45
|
+
'magiclink',
|
|
66
46
|
|
|
67
|
-
//
|
|
68
|
-
'
|
|
47
|
+
// Security codes
|
|
48
|
+
'securitycode', 'authcode', 'verificationcode',
|
|
49
|
+
'pincode', 'pin',
|
|
69
50
|
|
|
70
|
-
//
|
|
71
|
-
'
|
|
51
|
+
// Encryption
|
|
52
|
+
'encrypt', 'cipher', 'iv', 'nonce',
|
|
53
|
+
'hmac', 'signature',
|
|
72
54
|
];
|
|
73
55
|
|
|
74
|
-
//
|
|
75
|
-
this.
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
//
|
|
96
|
-
this.
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
//
|
|
128
|
-
this.
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
|
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.
|
|
101
|
+
console.error(`Error analyzing ${filePath}:`, error.message);
|
|
153
102
|
}
|
|
154
103
|
}
|
|
155
104
|
}
|
|
156
105
|
|
|
157
106
|
return violations;
|
|
158
107
|
}
|
|
159
|
-
|
|
160
|
-
|
|
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
|
|
111
|
+
const ext = path.extname(filePath);
|
|
174
112
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
//
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
//
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
//
|
|
246
|
-
|
|
247
|
-
|
|
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
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
containsSafePattern(line) {
|
|
255
|
-
return this.safePatterns.some(pattern => pattern.test(line));
|
|
245
|
+
return resolved;
|
|
256
246
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
|
|
283
|
-
const variableContext = this.getVariableContext(line);
|
|
284
|
-
if (variableContext && this.isSecurityVariable(variableContext)) {
|
|
285
|
-
return true;
|
|
286
|
-
}
|
|
299
|
+
if (!initializer) return null;
|
|
287
300
|
|
|
288
|
-
|
|
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
|
-
|
|
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
|
-
|
|
308
|
-
|
|
307
|
+
// If explicitly non-security, skip
|
|
308
|
+
if (isNonSecurityVar) {
|
|
309
|
+
return null;
|
|
309
310
|
}
|
|
310
311
|
|
|
311
|
-
// Check
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
//
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
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
|
|
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
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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:
|
|
355
|
-
line:
|
|
356
|
-
column:
|
|
352
|
+
message: `${reason} Use crypto.randomBytes() or crypto.randomUUID().`,
|
|
353
|
+
line: varDecl.getStartLineNumber(),
|
|
354
|
+
column: varDecl.getStart(),
|
|
357
355
|
filePath: filePath,
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
insecureFunction: mathRandomMatch[1]
|
|
356
|
+
context: varName,
|
|
357
|
+
...(traceInfo && { helperFunction: traceInfo.helperFunction })
|
|
361
358
|
};
|
|
362
359
|
}
|
|
363
360
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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
|
-
//
|
|
411
|
-
|
|
412
|
-
|
|
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
|
-
|
|
415
|
-
|
|
416
|
-
/Date\.now\(\).*(?:jwt|token|payload)/i,
|
|
417
|
-
];
|
|
468
|
+
current = current.getParent();
|
|
469
|
+
}
|
|
418
470
|
|
|
419
|
-
return
|
|
471
|
+
return false;
|
|
420
472
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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
|
-
|
|
441
|
-
|
|
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
|
-
//
|
|
453
|
-
|
|
454
|
-
if (
|
|
455
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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
|
-
|
|
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(
|
|
539
|
+
|
|
540
|
+
getSecureAlternatives(language = 'javascript') {
|
|
477
541
|
const alternatives = {
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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[
|
|
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
|
|