@sun-asterisk/sunlint 1.3.26 → 1.3.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config/rules/enhanced-rules-registry.json +101 -17
- package/config/rules/rules-registry-generated.json +22 -22
- package/origin-rules/security-en.md +351 -338
- package/package.json +1 -1
- package/rules/common/C003_no_vague_abbreviations/analyzer.js +73 -21
- package/rules/common/C017_constructor_logic/symbol-based-analyzer.js +206 -2
- package/rules/common/C024_no_scatter_hardcoded_constants/symbol-based-analyzer.js +553 -58
- package/rules/common/C029_catch_block_logging/analyzer.js +47 -12
- package/rules/common/C033_separate_service_repository/symbol-based-analyzer.js +35 -15
- package/rules/common/C041_no_sensitive_hardcode/symbol-based-analyzer.js +9 -5
- package/rules/security/S003_open_redirect_protection/README.md +371 -0
- package/rules/security/S003_open_redirect_protection/analyzer.js +135 -0
- package/rules/security/S003_open_redirect_protection/config.json +58 -0
- package/rules/security/S003_open_redirect_protection/symbol-based-analyzer.js +884 -0
- package/rules/security/S004_sensitive_data_logging/analyzer.js +135 -0
- package/rules/security/S004_sensitive_data_logging/config.json +62 -0
- package/rules/security/S004_sensitive_data_logging/symbol-based-analyzer.js +592 -0
- package/rules/security/S005_no_origin_auth/analyzer.js +97 -148
- package/rules/security/S005_no_origin_auth/config.json +28 -67
- package/rules/security/S005_no_origin_auth/symbol-based-analyzer.js +708 -0
- package/rules/security/S006_no_plaintext_recovery_codes/symbol-based-analyzer.js +170 -31
- package/rules/security/S010_no_insecure_encryption/analyzer.js +8 -2
- package/rules/security/S012_hardcoded_secrets/analyzer.js +149 -0
- package/rules/security/S012_hardcoded_secrets/config.json +75 -0
- package/rules/security/S012_hardcoded_secrets/symbol-based-analyzer.js +1204 -0
- package/rules/security/S013_tls_enforcement/symbol-based-analyzer.js +87 -0
- package/rules/security/S017_use_parameterized_queries/analyzer.js +11 -78
- package/rules/security/S017_use_parameterized_queries/symbol-based-analyzer.js +1146 -1
- package/rules/security/S019_smtp_injection_protection/analyzer.js +120 -0
- package/rules/security/S019_smtp_injection_protection/config.json +35 -0
- package/rules/security/S019_smtp_injection_protection/symbol-based-analyzer.js +687 -0
- package/rules/security/S020_no_eval_dynamic_code/analyzer.js +55 -130
- package/rules/security/S020_no_eval_dynamic_code/symbol-based-analyzer.js +4 -19
- package/rules/security/S022_escape_output_context/README.md +254 -0
- package/rules/security/S022_escape_output_context/analyzer.js +510 -0
- package/rules/security/S022_escape_output_context/config.json +229 -0
- package/rules/security/S023_no_json_injection/analyzer.js +15 -0
- package/rules/security/S023_no_json_injection/ast-analyzer.js +18 -3
- package/rules/security/S023_no_json_injection/config.json +133 -0
- package/rules/security/S024_xpath_xxe_protection/regex-based-analyzer.js +41 -0
- package/rules/security/S027_no_hardcoded_secrets/analyzer.js +67 -8
- package/rules/security/S027_no_hardcoded_secrets/categorized-analyzer.js +29 -6
- package/rules/security/S029_csrf_protection/config.json +127 -0
- package/rules/security/S030_directory_browsing_protection/regex-based-analyzer.js +160 -28
- package/rules/security/S030_directory_browsing_protection/symbol-based-analyzer.js +81 -19
- package/rules/security/S031_secure_session_cookies/analyzer.js +20 -2
- package/rules/security/S031_secure_session_cookies/regex-based-analyzer.js +100 -0
- package/rules/security/S031_secure_session_cookies/symbol-based-analyzer.js +8 -1
- package/rules/security/S032_httponly_session_cookies/analyzer.js +2 -2
- package/rules/security/S032_httponly_session_cookies/regex-based-analyzer.js +115 -0
- package/rules/security/S032_httponly_session_cookies/symbol-based-analyzer.js +39 -10
- package/rules/security/S036_lfi_rfi_protection/analyzer.js +224 -0
- package/rules/security/S036_lfi_rfi_protection/config.json +20 -0
- package/rules/security/S040_session_fixation_protection/analyzer.js +153 -0
- package/rules/security/S040_session_fixation_protection/config.json +20 -0
- package/rules/security/S042_require_re_authentication_for_long_lived/README.md +83 -0
- package/rules/security/S042_require_re_authentication_for_long_lived/analyzer.js +153 -0
- package/rules/security/S042_require_re_authentication_for_long_lived/config.json +41 -0
- package/rules/security/S042_require_re_authentication_for_long_lived/symbol-based-analyzer.js +1139 -0
- package/rules/security/S043_password_changes_invalidate_all_sessions/README.md +107 -0
- package/rules/security/S043_password_changes_invalidate_all_sessions/analyzer.js +153 -0
- package/rules/security/S043_password_changes_invalidate_all_sessions/config.json +41 -0
- package/rules/security/S043_password_changes_invalidate_all_sessions/symbol-based-analyzer.js +541 -0
- package/docs/COMMAND-EXAMPLES.md +0 -390
- package/docs/FILE_LIMITS_COMPLETION_REPORT.md +0 -151
- package/docs/FOLDER_STRUCTURE.md +0 -59
- package/docs/SIMPLIFIED_USAGE_GUIDE.md +0 -208
- package/rules/security/S017_use_parameterized_queries/regex-based-analyzer.js +0 -541
- package/rules/security/S020_no_eval_dynamic_code/regex-based-analyzer.js +0 -307
|
@@ -0,0 +1,1139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S042
|
|
3
|
+
* Require re-authentication for long-lived sessions or sensitive actions
|
|
4
|
+
* Objective: Reduce the risk of session hijacking or privilege misuse by forcing
|
|
5
|
+
* re-authentication after long idle periods or before critical actions.
|
|
6
|
+
* 1. Excessive Session Duration: Detects sessions configured for more than 24 hours
|
|
7
|
+
* 2. JWT Without Expiration: Identifies JWT tokens created without expiry times
|
|
8
|
+
* 3. Excessive JWT Expiration: Flags access tokens with expiry > 1 hour
|
|
9
|
+
* 4. Sensitive Actions Without Re-auth: Detects functions like password changes without re-authentication
|
|
10
|
+
* 5. Persistent Sessions Without Re-auth: Flags "Remember Me" features without proper security
|
|
11
|
+
* 6. Missing Idle Timeout: Identifies session configurations without inactivity timeouts
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const { SyntaxKind } = require('ts-morph');
|
|
15
|
+
|
|
16
|
+
class S042SymbolBasedAnalyzer {
|
|
17
|
+
constructor(semanticEngine = null) {
|
|
18
|
+
this.ruleId = "S042";
|
|
19
|
+
this.ruleName = 'Require re-authentication for long-lived sessions or sensitive actions';
|
|
20
|
+
this.semanticEngine = semanticEngine;
|
|
21
|
+
this.verbose = false;
|
|
22
|
+
this.skipPatterns = [
|
|
23
|
+
/\/node_modules\//,
|
|
24
|
+
/\/tests?\//,
|
|
25
|
+
/\/dist\//,
|
|
26
|
+
/\/build\//,
|
|
27
|
+
/\.spec\.ts$/,
|
|
28
|
+
/\.test\.ts$/
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
// Sensitive action patterns
|
|
32
|
+
this.sensitiveActions = [
|
|
33
|
+
/password.*change/i,
|
|
34
|
+
/update.*password/i,
|
|
35
|
+
/change.*password/i,
|
|
36
|
+
/reset.*password/i,
|
|
37
|
+
/delete.*account/i,
|
|
38
|
+
/remove.*account/i,
|
|
39
|
+
/payment/i,
|
|
40
|
+
/transfer.*fund/i,
|
|
41
|
+
/withdraw/i,
|
|
42
|
+
/deposit/i,
|
|
43
|
+
/update.*email/i,
|
|
44
|
+
/change.*email/i,
|
|
45
|
+
/two.*factor/i,
|
|
46
|
+
/2fa/i,
|
|
47
|
+
/security.*setting/i,
|
|
48
|
+
/role.*change/i,
|
|
49
|
+
/admin.*action/i
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
// Session MIDDLEWARE configuration keys (express-session, cookie-session, etc.)
|
|
53
|
+
this.sessionConfigKeys = [
|
|
54
|
+
'secret',
|
|
55
|
+
'resave',
|
|
56
|
+
'saveUninitialized',
|
|
57
|
+
'cookie',
|
|
58
|
+
'rolling',
|
|
59
|
+
'store',
|
|
60
|
+
'genid',
|
|
61
|
+
'name',
|
|
62
|
+
'proxy',
|
|
63
|
+
'unset'
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
// JWT configuration patterns
|
|
67
|
+
this.jwtConfigKeys = [
|
|
68
|
+
'expiresIn',
|
|
69
|
+
'expiry',
|
|
70
|
+
'exp',
|
|
71
|
+
'maxAge',
|
|
72
|
+
'tokenExpiration'
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
// Time thresholds (in milliseconds)
|
|
76
|
+
this.MAX_SESSION_AGE = 24 * 60 * 60 * 1000; // 24 hours
|
|
77
|
+
this.MAX_IDLE_TIME = 30 * 60 * 1000; // 30 minutes
|
|
78
|
+
this.MAX_JWT_EXPIRY = 60 * 60 * 1000; // 1 hour for access tokens
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async initialize(semanticEngine = null) {
|
|
82
|
+
if (semanticEngine) {
|
|
83
|
+
this.semanticEngine = semanticEngine;
|
|
84
|
+
}
|
|
85
|
+
this.verbose = semanticEngine?.verbose || false;
|
|
86
|
+
|
|
87
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
88
|
+
console.log(`🔧 [S042 Symbol-Based] Analyzer initialized, verbose: ${this.verbose}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async analyzeFileBasic(filePath, options = {}) {
|
|
93
|
+
return await this.analyzeFileWithSymbols(filePath, options);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
analyzeFileWithSymbols(filePath, options = {}) {
|
|
97
|
+
const violations = [];
|
|
98
|
+
const verbose = options.verbose || this.verbose;
|
|
99
|
+
|
|
100
|
+
if (!this.semanticEngine?.project) {
|
|
101
|
+
if (verbose) {
|
|
102
|
+
console.warn('[S042 Symbol-Based] No semantic engine available, skipping analysis');
|
|
103
|
+
}
|
|
104
|
+
return violations;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (this.shouldIgnoreFile(filePath)) {
|
|
108
|
+
if (verbose) console.log(`[${this.ruleId}] Ignoring ${filePath}`);
|
|
109
|
+
return violations;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (verbose) {
|
|
113
|
+
console.log(`🔍 [S042 Symbol-Based] Starting analysis for ${filePath}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const sourceFile = this.semanticEngine.project.getSourceFile(filePath);
|
|
118
|
+
if (!sourceFile) {
|
|
119
|
+
return violations;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Check for session configuration issues
|
|
123
|
+
violations.push(...this.checkSessionConfiguration(sourceFile, verbose));
|
|
124
|
+
|
|
125
|
+
// Check for JWT configuration issues
|
|
126
|
+
violations.push(...this.checkJWTConfiguration(sourceFile, verbose));
|
|
127
|
+
|
|
128
|
+
// Check for sensitive actions without re-authentication
|
|
129
|
+
violations.push(...this.checkSensitiveActions(sourceFile, verbose));
|
|
130
|
+
|
|
131
|
+
// Check for Remember Me implementations
|
|
132
|
+
violations.push(...this.checkRememberMeImplementation(sourceFile, verbose));
|
|
133
|
+
|
|
134
|
+
// Check for idle timeout implementation
|
|
135
|
+
violations.push(...this.checkIdleTimeoutImplementation(sourceFile, verbose));
|
|
136
|
+
|
|
137
|
+
return violations;
|
|
138
|
+
} catch (error) {
|
|
139
|
+
if (verbose) {
|
|
140
|
+
console.warn(`[S042 Symbol-Based] Analysis failed for ${filePath}:`, error.message);
|
|
141
|
+
}
|
|
142
|
+
return violations;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
checkSessionConfiguration(sourceFile, verbose) {
|
|
147
|
+
const violations = [];
|
|
148
|
+
|
|
149
|
+
// Find all object literals that might be session configuration
|
|
150
|
+
const objectLiterals = sourceFile.getDescendantsOfKind(SyntaxKind.ObjectLiteralExpression);
|
|
151
|
+
|
|
152
|
+
for (const objLiteral of objectLiterals) {
|
|
153
|
+
// Get context to understand what this object is for
|
|
154
|
+
const context = this.getObjectContext(objLiteral);
|
|
155
|
+
|
|
156
|
+
// Skip if this is clearly not a session middleware config
|
|
157
|
+
if (this.isSessionDataModel(context, objLiteral)) {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const properties = objLiteral.getProperties();
|
|
162
|
+
const sessionConfig = this.extractSessionConfig(properties, objLiteral, context);
|
|
163
|
+
|
|
164
|
+
// Only process if this is actually a session MIDDLEWARE configuration
|
|
165
|
+
if (sessionConfig.isSessionConfig) {
|
|
166
|
+
// Check for excessively long session duration
|
|
167
|
+
if (sessionConfig.maxAge && sessionConfig.maxAge > this.MAX_SESSION_AGE) {
|
|
168
|
+
const node = sessionConfig.maxAgeNode;
|
|
169
|
+
const startLine = node.getStartLineNumber();
|
|
170
|
+
|
|
171
|
+
violations.push({
|
|
172
|
+
ruleId: this.ruleId,
|
|
173
|
+
ruleName: this.ruleName,
|
|
174
|
+
severity: 'medium',
|
|
175
|
+
message: `Session maxAge is too long (${this.formatDuration(sessionConfig.maxAge)}). Consider limiting to 24 hours or less for security.`,
|
|
176
|
+
line: startLine,
|
|
177
|
+
column: node.getStart() - node.getStartLinePos() + 1,
|
|
178
|
+
filePath: sourceFile.getFilePath(),
|
|
179
|
+
type: 'EXCESSIVE_SESSION_DURATION',
|
|
180
|
+
details: `Long-lived sessions increase the risk of session hijacking. Recommended maximum: ${this.formatDuration(this.MAX_SESSION_AGE)}`
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Check if persistent/rememberMe is enabled without limits
|
|
185
|
+
if (sessionConfig.isPersistent && !sessionConfig.hasReauthCheck) {
|
|
186
|
+
const node = sessionConfig.persistentNode;
|
|
187
|
+
const startLine = node.getStartLineNumber();
|
|
188
|
+
|
|
189
|
+
violations.push({
|
|
190
|
+
ruleId: this.ruleId,
|
|
191
|
+
ruleName: this.ruleName,
|
|
192
|
+
severity: 'medium',
|
|
193
|
+
message: `Persistent session (Remember Me) enabled without re-authentication requirements for sensitive actions.`,
|
|
194
|
+
line: startLine,
|
|
195
|
+
column: node.getStart() - node.getStartLinePos() + 1,
|
|
196
|
+
filePath: sourceFile.getFilePath(),
|
|
197
|
+
type: 'PERSISTENT_SESSION_WITHOUT_REAUTH',
|
|
198
|
+
details: 'Persistent sessions should require re-authentication for sensitive operations like password changes or payments.'
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Check for missing idle timeout
|
|
203
|
+
if (!sessionConfig.hasIdleTimeout) {
|
|
204
|
+
const node = objLiteral;
|
|
205
|
+
const startLine = node.getStartLineNumber();
|
|
206
|
+
|
|
207
|
+
violations.push({
|
|
208
|
+
ruleId: this.ruleId,
|
|
209
|
+
ruleName: this.ruleName,
|
|
210
|
+
severity: 'medium',
|
|
211
|
+
message: `Session configuration missing idle timeout. Consider adding inactivity-based expiration.`,
|
|
212
|
+
line: startLine,
|
|
213
|
+
column: node.getStart() - node.getStartLinePos() + 1,
|
|
214
|
+
filePath: sourceFile.getFilePath(),
|
|
215
|
+
type: 'MISSING_IDLE_TIMEOUT',
|
|
216
|
+
details: `Recommended idle timeout: ${this.formatDuration(this.MAX_IDLE_TIME)} or less. Use 'rolling: true' or 'idleTimeout' option.`
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return violations;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
isSessionDataModel(context, objLiteral) {
|
|
226
|
+
// Check if this is a data model object (not middleware config)
|
|
227
|
+
if (context) {
|
|
228
|
+
// Repository/database operations
|
|
229
|
+
if (context.match(/repository|repo|model|entity|database|db|create|save|update|insert/i)) {
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Check if object has data model indicators
|
|
235
|
+
const properties = objLiteral.getProperties();
|
|
236
|
+
const propNames = properties
|
|
237
|
+
.filter(p => p.getKind() === SyntaxKind.PropertyAssignment)
|
|
238
|
+
.map(p => p.getName());
|
|
239
|
+
|
|
240
|
+
// Data model indicators
|
|
241
|
+
const dataModelKeys = ['userId', 'token', 'expiresAt', 'createdAt', 'updatedAt', 'id'];
|
|
242
|
+
const hasDataModelKeys = propNames.filter(name =>
|
|
243
|
+
dataModelKeys.some(key => name === key)
|
|
244
|
+
).length >= 2;
|
|
245
|
+
|
|
246
|
+
return hasDataModelKeys;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
extractSessionConfig(properties, objLiteral, context) {
|
|
250
|
+
const config = {
|
|
251
|
+
isSessionConfig: false,
|
|
252
|
+
maxAge: null,
|
|
253
|
+
maxAgeNode: null,
|
|
254
|
+
isPersistent: false,
|
|
255
|
+
persistentNode: null,
|
|
256
|
+
hasReauthCheck: false,
|
|
257
|
+
hasIdleTimeout: false
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
let sessionKeyCount = 0;
|
|
261
|
+
let jwtKeyCount = 0;
|
|
262
|
+
|
|
263
|
+
for (const prop of properties) {
|
|
264
|
+
if (prop.getKind() !== SyntaxKind.PropertyAssignment) continue;
|
|
265
|
+
|
|
266
|
+
const name = prop.getName();
|
|
267
|
+
const initializer = prop.getInitializer();
|
|
268
|
+
|
|
269
|
+
// Count session-specific keys
|
|
270
|
+
if (this.sessionConfigKeys.some(key => name === key)) {
|
|
271
|
+
sessionKeyCount++;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Count JWT-specific keys
|
|
275
|
+
if (this.jwtConfigKeys.some(key => name === key)) {
|
|
276
|
+
jwtKeyCount++;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Check maxAge/expires in cookie object
|
|
280
|
+
if (name === 'cookie' && initializer && initializer.getKind() === SyntaxKind.ObjectLiteralExpression) {
|
|
281
|
+
const cookieProps = initializer.getProperties();
|
|
282
|
+
for (const cookieProp of cookieProps) {
|
|
283
|
+
if (cookieProp.getKind() !== SyntaxKind.PropertyAssignment) continue;
|
|
284
|
+
|
|
285
|
+
const cookiePropName = cookieProp.getName();
|
|
286
|
+
const cookieInitializer = cookieProp.getInitializer();
|
|
287
|
+
|
|
288
|
+
if (cookiePropName === 'maxAge') {
|
|
289
|
+
const value = this.extractNumericValue(cookieInitializer);
|
|
290
|
+
if (value !== null) {
|
|
291
|
+
config.maxAge = value;
|
|
292
|
+
config.maxAgeNode = cookieProp;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Check for persistent/rememberMe (but only if not in a data model context)
|
|
299
|
+
if (name.match(/persistent|rememberMe/i) && name !== 'requiresReauthForSensitiveActions') {
|
|
300
|
+
const value = this.extractBooleanValue(initializer);
|
|
301
|
+
if (value === true) {
|
|
302
|
+
config.isPersistent = true;
|
|
303
|
+
config.persistentNode = prop;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Check for idle timeout or rolling sessions
|
|
308
|
+
if (name.match(/idleTimeout|idle/i)) {
|
|
309
|
+
config.hasIdleTimeout = true;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (name === 'rolling') {
|
|
313
|
+
const value = this.extractBooleanValue(initializer);
|
|
314
|
+
if (value === true) {
|
|
315
|
+
config.hasIdleTimeout = true;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Check for re-authentication flags
|
|
320
|
+
if (name.match(/reauth|reauthenticate|requireAuth|requiresReauthForSensitiveActions/i)) {
|
|
321
|
+
config.hasReauthCheck = true;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Determine if this is a session MIDDLEWARE config based on:
|
|
326
|
+
// 1. Has multiple session-specific keys
|
|
327
|
+
// 2. Context indicates session middleware
|
|
328
|
+
// 3. NOT dominated by JWT keys
|
|
329
|
+
if (sessionKeyCount >= 2 && jwtKeyCount < sessionKeyCount) {
|
|
330
|
+
config.isSessionConfig = true;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Strong indicator: context mentions session middleware
|
|
334
|
+
if (context && context.match(/session\(|express.*session|cookie.*session|getSessionConfig|sessionConfig|sessionOptions|sessionMiddleware/i)) {
|
|
335
|
+
// Only mark as session config if it has at least one session key
|
|
336
|
+
if (sessionKeyCount >= 1) {
|
|
337
|
+
config.isSessionConfig = true;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return config;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
getObjectContext(objLiteral) {
|
|
345
|
+
// Get the parent context to understand what this object is for
|
|
346
|
+
let parent = objLiteral.getParent();
|
|
347
|
+
let depth = 0;
|
|
348
|
+
|
|
349
|
+
while (parent && depth < 5) {
|
|
350
|
+
const kind = parent.getKind();
|
|
351
|
+
|
|
352
|
+
// Check variable declaration
|
|
353
|
+
if (kind === SyntaxKind.VariableDeclaration) {
|
|
354
|
+
const name = parent.getNameNode()?.getText();
|
|
355
|
+
return name;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Check call expression (e.g., session(config), repository.create(data))
|
|
359
|
+
if (kind === SyntaxKind.CallExpression) {
|
|
360
|
+
const expr = parent.getExpression();
|
|
361
|
+
return expr.getText();
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Check property assignment
|
|
365
|
+
if (kind === SyntaxKind.PropertyAssignment) {
|
|
366
|
+
return parent.getName();
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Check method declaration
|
|
370
|
+
if (kind === SyntaxKind.MethodDeclaration) {
|
|
371
|
+
const name = parent.getNameNode()?.getText();
|
|
372
|
+
return name;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Check return statement
|
|
376
|
+
if (kind === SyntaxKind.ReturnStatement) {
|
|
377
|
+
// Look for the parent function name
|
|
378
|
+
const func = parent.getParent();
|
|
379
|
+
if (func && (func.getKind() === SyntaxKind.MethodDeclaration ||
|
|
380
|
+
func.getKind() === SyntaxKind.FunctionDeclaration)) {
|
|
381
|
+
const funcName = func.getNameNode()?.getText();
|
|
382
|
+
return funcName;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
parent = parent.getParent();
|
|
387
|
+
depth++;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
checkJWTConfiguration(sourceFile, verbose) {
|
|
394
|
+
const violations = [];
|
|
395
|
+
|
|
396
|
+
// Find JWT signing/creation patterns
|
|
397
|
+
const callExpressions = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);
|
|
398
|
+
|
|
399
|
+
for (const callExpr of callExpressions) {
|
|
400
|
+
const expression = callExpr.getExpression();
|
|
401
|
+
const expressionText = expression.getText();
|
|
402
|
+
|
|
403
|
+
// Check for jwt.sign() or similar
|
|
404
|
+
if (this.isJWTSignCall(expressionText)) {
|
|
405
|
+
const args = callExpr.getArguments();
|
|
406
|
+
|
|
407
|
+
if (args.length === 0) {
|
|
408
|
+
continue; // Skip if no arguments
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Determine which argument contains options
|
|
412
|
+
const optionsArg = this.findJWTOptionsArgument(args, expressionText);
|
|
413
|
+
|
|
414
|
+
if (!optionsArg) {
|
|
415
|
+
// No options argument found at all
|
|
416
|
+
const startLine = callExpr.getStartLineNumber();
|
|
417
|
+
|
|
418
|
+
violations.push({
|
|
419
|
+
ruleId: this.ruleId,
|
|
420
|
+
ruleName: this.ruleName,
|
|
421
|
+
severity: 'high',
|
|
422
|
+
message: `JWT token created without expiration configuration. Add expiresIn option.`,
|
|
423
|
+
line: startLine,
|
|
424
|
+
column: callExpr.getStart() - callExpr.getStartLinePos() + 1,
|
|
425
|
+
filePath: sourceFile.getFilePath(),
|
|
426
|
+
type: 'JWT_MISSING_OPTIONS',
|
|
427
|
+
details: 'Example: jwt.sign(payload, secret, { expiresIn: "1h" }) or jwtService.sign(payload, { expiresIn: "1h" })'
|
|
428
|
+
});
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Check if options argument is an object literal
|
|
433
|
+
if (optionsArg.getKind() === SyntaxKind.ObjectLiteralExpression) {
|
|
434
|
+
const jwtConfig = this.extractJWTConfig(optionsArg);
|
|
435
|
+
|
|
436
|
+
// Check for missing expiration
|
|
437
|
+
if (!jwtConfig.hasExpiration) {
|
|
438
|
+
const startLine = callExpr.getStartLineNumber();
|
|
439
|
+
|
|
440
|
+
violations.push({
|
|
441
|
+
ruleId: this.ruleId,
|
|
442
|
+
ruleName: this.ruleName,
|
|
443
|
+
severity: 'high',
|
|
444
|
+
message: `JWT token created without expiration time. Tokens should have short expiry (e.g., 1 hour).`,
|
|
445
|
+
line: startLine,
|
|
446
|
+
column: callExpr.getStart() - callExpr.getStartLinePos() + 1,
|
|
447
|
+
filePath: sourceFile.getFilePath(),
|
|
448
|
+
type: 'JWT_WITHOUT_EXPIRATION',
|
|
449
|
+
details: 'JWTs without expiration can be used indefinitely if compromised. Use short-lived access tokens with refresh token mechanism.'
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Check for excessively long JWT expiration (only for access tokens, not refresh tokens)
|
|
454
|
+
if (jwtConfig.expirationValue && jwtConfig.expirationValue > this.MAX_JWT_EXPIRY) {
|
|
455
|
+
// Check if this might be a refresh token (look for 'refresh' in variable names or comments)
|
|
456
|
+
const isLikelyRefreshToken = this.isLikelyRefreshToken(callExpr);
|
|
457
|
+
|
|
458
|
+
if (!isLikelyRefreshToken) {
|
|
459
|
+
const startLine = jwtConfig.expirationNode.getStartLineNumber();
|
|
460
|
+
|
|
461
|
+
violations.push({
|
|
462
|
+
ruleId: this.ruleId,
|
|
463
|
+
ruleName: this.ruleName,
|
|
464
|
+
severity: 'medium',
|
|
465
|
+
message: `JWT expiration is too long (${this.formatDuration(jwtConfig.expirationValue)}). Access tokens should expire within 1 hour.`,
|
|
466
|
+
line: startLine,
|
|
467
|
+
column: jwtConfig.expirationNode.getStart() - jwtConfig.expirationNode.getStartLinePos() + 1,
|
|
468
|
+
filePath: sourceFile.getFilePath(),
|
|
469
|
+
type: 'EXCESSIVE_JWT_EXPIRATION',
|
|
470
|
+
details: `Use short-lived access tokens (${this.formatDuration(this.MAX_JWT_EXPIRY)} or less) with refresh token rotation.`
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return violations;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
findJWTOptionsArgument(args, expressionText) {
|
|
482
|
+
// For @nestjs/jwt JwtService.sign(payload, options)
|
|
483
|
+
// The options is the second argument (index 1)
|
|
484
|
+
if (expressionText.includes('jwtService') || expressionText.includes('JwtService')) {
|
|
485
|
+
return args[1] || null;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// For jsonwebtoken jwt.sign(payload, secret, options)
|
|
489
|
+
// The options is the third argument (index 2)
|
|
490
|
+
if (expressionText.match(/jwt\.sign|JWT\.sign/)) {
|
|
491
|
+
return args[2] || null;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Default: check both positions
|
|
495
|
+
// Try third argument first (jsonwebtoken style)
|
|
496
|
+
if (args[2] && args[2].getKind() === SyntaxKind.ObjectLiteralExpression) {
|
|
497
|
+
return args[2];
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Try second argument (NestJS style)
|
|
501
|
+
if (args[1] && args[1].getKind() === SyntaxKind.ObjectLiteralExpression) {
|
|
502
|
+
return args[1];
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
return null;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
isLikelyRefreshToken(callExpr) {
|
|
509
|
+
// Check parent variable declaration
|
|
510
|
+
const parent = callExpr.getParent();
|
|
511
|
+
|
|
512
|
+
if (parent) {
|
|
513
|
+
// Check variable name
|
|
514
|
+
if (parent.getKind() === SyntaxKind.VariableDeclaration) {
|
|
515
|
+
const varName = parent.getNameNode()?.getText() || '';
|
|
516
|
+
if (varName.toLowerCase().includes('refresh')) {
|
|
517
|
+
return true;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Check property assignment
|
|
522
|
+
if (parent.getKind() === SyntaxKind.PropertyAssignment) {
|
|
523
|
+
const propName = parent.getName();
|
|
524
|
+
if (propName.toLowerCase().includes('refresh')) {
|
|
525
|
+
return true;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Check preceding comments
|
|
531
|
+
const sourceFile = callExpr.getSourceFile();
|
|
532
|
+
const fullText = sourceFile.getFullText();
|
|
533
|
+
const pos = callExpr.getStart();
|
|
534
|
+
const precedingText = fullText.substring(Math.max(0, pos - 200), pos);
|
|
535
|
+
|
|
536
|
+
if (precedingText.toLowerCase().includes('refresh')) {
|
|
537
|
+
return true;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
return false;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
extractJWTConfig(optionsNode) {
|
|
544
|
+
const config = {
|
|
545
|
+
hasExpiration: false,
|
|
546
|
+
expirationValue: null,
|
|
547
|
+
expirationNode: null
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
if (optionsNode.getKind() !== SyntaxKind.ObjectLiteralExpression) {
|
|
551
|
+
return config;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const properties = optionsNode.getProperties();
|
|
555
|
+
|
|
556
|
+
for (const prop of properties) {
|
|
557
|
+
if (prop.getKind() !== SyntaxKind.PropertyAssignment) continue;
|
|
558
|
+
|
|
559
|
+
const name = prop.getName();
|
|
560
|
+
const initializer = prop.getInitializer();
|
|
561
|
+
|
|
562
|
+
if (this.jwtConfigKeys.some(key => name === key)) {
|
|
563
|
+
config.hasExpiration = true;
|
|
564
|
+
config.expirationNode = prop;
|
|
565
|
+
|
|
566
|
+
// Try to extract numeric value
|
|
567
|
+
const value = this.extractTimeValue(initializer);
|
|
568
|
+
if (value !== null) {
|
|
569
|
+
config.expirationValue = value;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
return config;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
checkSensitiveActions(sourceFile, verbose) {
|
|
578
|
+
const violations = [];
|
|
579
|
+
|
|
580
|
+
// Find function/method declarations
|
|
581
|
+
const functions = [
|
|
582
|
+
...sourceFile.getDescendantsOfKind(SyntaxKind.FunctionDeclaration),
|
|
583
|
+
...sourceFile.getDescendantsOfKind(SyntaxKind.MethodDeclaration),
|
|
584
|
+
...sourceFile.getDescendantsOfKind(SyntaxKind.ArrowFunction)
|
|
585
|
+
];
|
|
586
|
+
|
|
587
|
+
for (const func of functions) {
|
|
588
|
+
const funcName = this.getFunctionName(func);
|
|
589
|
+
|
|
590
|
+
if (!funcName) continue;
|
|
591
|
+
|
|
592
|
+
// Check if function name suggests sensitive action
|
|
593
|
+
if (this.isSensitiveAction(funcName)) {
|
|
594
|
+
const hasReauth = this.checkForReauthentication(func);
|
|
595
|
+
|
|
596
|
+
if (!hasReauth) {
|
|
597
|
+
const startLine = func.getStartLineNumber();
|
|
598
|
+
|
|
599
|
+
// Provide more context in the message
|
|
600
|
+
const params = func.getParameters().map(p => p.getName()).join(', ');
|
|
601
|
+
const hasPasswordParam = params.match(/password|auth|verify|confirm/i);
|
|
602
|
+
|
|
603
|
+
let suggestion = 'Sensitive operations like password changes, payments, or account deletion should require recent authentication or password verification.';
|
|
604
|
+
|
|
605
|
+
if (!hasPasswordParam) {
|
|
606
|
+
suggestion += ' Consider adding a currentPassword or twoFactorCode parameter.';
|
|
607
|
+
} else {
|
|
608
|
+
suggestion += ' Ensure the password parameter is verified against the user\'s current password.';
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
violations.push({
|
|
612
|
+
ruleId: this.ruleId,
|
|
613
|
+
ruleName: this.ruleName,
|
|
614
|
+
severity: 'high',
|
|
615
|
+
message: `Sensitive action '${funcName}' does not appear to require re-authentication.`,
|
|
616
|
+
line: startLine,
|
|
617
|
+
column: func.getStart() - func.getStartLinePos() + 1,
|
|
618
|
+
filePath: sourceFile.getFilePath(),
|
|
619
|
+
type: 'SENSITIVE_ACTION_WITHOUT_REAUTH',
|
|
620
|
+
details: suggestion
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
return violations;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
checkRememberMeImplementation(sourceFile, verbose) {
|
|
630
|
+
const violations = [];
|
|
631
|
+
|
|
632
|
+
const allNodes = sourceFile.getDescendantsOfKind(SyntaxKind.Identifier);
|
|
633
|
+
const processedLines = new Set(); // Avoid duplicate violations on same line
|
|
634
|
+
|
|
635
|
+
for (const node of allNodes) {
|
|
636
|
+
const text = node.getText();
|
|
637
|
+
|
|
638
|
+
if (text.match(/rememberMe|remember_me|persistentLogin/i)) {
|
|
639
|
+
const startLine = node.getStartLineNumber();
|
|
640
|
+
|
|
641
|
+
// Skip if already reported on this line
|
|
642
|
+
if (processedLines.has(startLine)) {
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// Check if there's nearby re-authentication logic
|
|
647
|
+
const parent = this.findRelevantParent(node);
|
|
648
|
+
|
|
649
|
+
if (parent) {
|
|
650
|
+
const hasReauthLogic = this.hasReauthenticationLogic(parent);
|
|
651
|
+
|
|
652
|
+
if (!hasReauthLogic) {
|
|
653
|
+
processedLines.add(startLine);
|
|
654
|
+
|
|
655
|
+
violations.push({
|
|
656
|
+
ruleId: this.ruleId,
|
|
657
|
+
ruleName: this.ruleName,
|
|
658
|
+
severity: 'medium',
|
|
659
|
+
message: `Remember Me feature implemented without apparent re-authentication requirements.`,
|
|
660
|
+
line: startLine,
|
|
661
|
+
column: node.getStart() - node.getStartLinePos() + 1,
|
|
662
|
+
filePath: sourceFile.getFilePath(),
|
|
663
|
+
type: 'REMEMBER_ME_WITHOUT_REAUTH',
|
|
664
|
+
details: 'Remember Me sessions should require re-authentication after a maximum period (e.g., 24 hours) or before sensitive actions.'
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
return violations;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
checkIdleTimeoutImplementation(sourceFile, verbose) {
|
|
675
|
+
const violations = [];
|
|
676
|
+
|
|
677
|
+
// Look for session middleware or configuration
|
|
678
|
+
const hasSessionConfig = this.hasSessionConfiguration(sourceFile);
|
|
679
|
+
const hasIdleTimeout = this.hasIdleTimeoutLogic(sourceFile);
|
|
680
|
+
|
|
681
|
+
if (hasSessionConfig && !hasIdleTimeout) {
|
|
682
|
+
violations.push({
|
|
683
|
+
ruleId: this.ruleId,
|
|
684
|
+
ruleName: this.ruleName,
|
|
685
|
+
severity: 'medium',
|
|
686
|
+
message: `Session management detected but no idle timeout implementation found.`,
|
|
687
|
+
line: 1,
|
|
688
|
+
column: 1,
|
|
689
|
+
filePath: sourceFile.getFilePath(),
|
|
690
|
+
type: 'MISSING_IDLE_TIMEOUT_LOGIC',
|
|
691
|
+
details: `Implement idle timeout to automatically expire sessions after ${this.formatDuration(this.MAX_IDLE_TIME)} of inactivity.`
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
return violations;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// Helper methods
|
|
699
|
+
|
|
700
|
+
isJWTSignCall(expressionText) {
|
|
701
|
+
return expressionText.match(/jwt\.sign|jsonwebtoken\.sign|JWT\.sign|jwtService\.sign|JwtService\.sign/i) !== null;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
isSensitiveAction(name) {
|
|
705
|
+
return this.sensitiveActions.some(pattern => pattern.test(name));
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
checkForReauthentication(functionNode) {
|
|
709
|
+
// First check: Does the function accept re-authentication parameters?
|
|
710
|
+
if (this.hasReauthenticationParameters(functionNode)) {
|
|
711
|
+
return true;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// Second check: Does the function body contain re-authentication logic?
|
|
715
|
+
const body = functionNode.getBody();
|
|
716
|
+
if (!body) return false;
|
|
717
|
+
|
|
718
|
+
const bodyText = body.getText();
|
|
719
|
+
|
|
720
|
+
// Direct re-authentication patterns
|
|
721
|
+
const reauthPatterns = [
|
|
722
|
+
/verifyPassword/i,
|
|
723
|
+
/checkPassword/i,
|
|
724
|
+
/requireAuth/i,
|
|
725
|
+
/reauth/i,
|
|
726
|
+
/verify.*credential/i,
|
|
727
|
+
/confirm.*password/i,
|
|
728
|
+
/validate.*password/i,
|
|
729
|
+
/authenticate.*again/i,
|
|
730
|
+
/require.*2fa/i,
|
|
731
|
+
/verify.*2fa/i,
|
|
732
|
+
/bcrypt\.compare/i,
|
|
733
|
+
/compareSync/i,
|
|
734
|
+
/passwordConfirmation/i,
|
|
735
|
+
/currentPassword/i,
|
|
736
|
+
/twoFactorCode/i
|
|
737
|
+
];
|
|
738
|
+
|
|
739
|
+
if (reauthPatterns.some(pattern => pattern.test(bodyText))) {
|
|
740
|
+
return true;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// Third check: Does it call service methods that likely handle re-authentication?
|
|
744
|
+
if (this.callsReauthenticationService(functionNode)) {
|
|
745
|
+
return true;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
return false;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
hasReauthenticationParameters(functionNode) {
|
|
752
|
+
const parameters = functionNode.getParameters();
|
|
753
|
+
|
|
754
|
+
const reauthParamNames = [
|
|
755
|
+
'currentPassword',
|
|
756
|
+
'password',
|
|
757
|
+
'passwordConfirmation',
|
|
758
|
+
'oldPassword',
|
|
759
|
+
'verifyPassword',
|
|
760
|
+
'confirmPassword',
|
|
761
|
+
'twoFactorCode',
|
|
762
|
+
'twoFactorToken',
|
|
763
|
+
'otpCode',
|
|
764
|
+
'mfaCode',
|
|
765
|
+
'authCode',
|
|
766
|
+
'verificationCode'
|
|
767
|
+
];
|
|
768
|
+
|
|
769
|
+
for (const param of parameters) {
|
|
770
|
+
const paramName = param.getName();
|
|
771
|
+
|
|
772
|
+
// Check if parameter name suggests re-authentication
|
|
773
|
+
if (reauthParamNames.some(name =>
|
|
774
|
+
paramName.toLowerCase().includes(name.toLowerCase())
|
|
775
|
+
)) {
|
|
776
|
+
return true;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// Check parameter type annotations for password-related types
|
|
780
|
+
const typeNode = param.getTypeNode();
|
|
781
|
+
if (typeNode) {
|
|
782
|
+
const typeText = typeNode.getText();
|
|
783
|
+
if (typeText.match(/password|auth|verification|2fa|mfa|otp/i)) {
|
|
784
|
+
return true;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
return false;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
callsReauthenticationService(functionNode) {
|
|
793
|
+
const body = functionNode.getBody();
|
|
794
|
+
if (!body) return false;
|
|
795
|
+
|
|
796
|
+
// Get all call expressions in the function
|
|
797
|
+
const callExpressions = body.getDescendantsOfKind(SyntaxKind.CallExpression);
|
|
798
|
+
|
|
799
|
+
for (const callExpr of callExpressions) {
|
|
800
|
+
const expression = callExpr.getExpression();
|
|
801
|
+
const callText = expression.getText();
|
|
802
|
+
|
|
803
|
+
// Check if calling a service method that handles re-authentication
|
|
804
|
+
const servicePatterns = [
|
|
805
|
+
/verifyPassword/i,
|
|
806
|
+
/checkPassword/i,
|
|
807
|
+
/validatePassword/i,
|
|
808
|
+
/authenticateUser/i,
|
|
809
|
+
/verifyCredentials/i,
|
|
810
|
+
/verify2FA/i,
|
|
811
|
+
/validateOTP/i,
|
|
812
|
+
/checkAuth/i,
|
|
813
|
+
/requireReauth/i,
|
|
814
|
+
/confirmIdentity/i,
|
|
815
|
+
/\.authenticate\(/i,
|
|
816
|
+
/\.verify\(/i,
|
|
817
|
+
/authService\./i,
|
|
818
|
+
/passwordService\./i,
|
|
819
|
+
/securityService\./i
|
|
820
|
+
];
|
|
821
|
+
|
|
822
|
+
if (servicePatterns.some(pattern => pattern.test(callText))) {
|
|
823
|
+
return true;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// Check if the call passes password-related arguments
|
|
827
|
+
const args = callExpr.getArguments();
|
|
828
|
+
for (const arg of args) {
|
|
829
|
+
const argText = arg.getText();
|
|
830
|
+
if (argText.match(/currentPassword|passwordConfirmation|twoFactorCode|oldPassword/i)) {
|
|
831
|
+
return true;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// Try to resolve the called function and check its implementation
|
|
836
|
+
if (this.semanticEngine?.project) {
|
|
837
|
+
const resolvedFunction = this.tryResolveCalledFunction(callExpr);
|
|
838
|
+
if (resolvedFunction) {
|
|
839
|
+
// Recursively check if the called function has re-authentication logic
|
|
840
|
+
const resolvedBody = resolvedFunction.getBody();
|
|
841
|
+
if (resolvedBody) {
|
|
842
|
+
const resolvedBodyText = resolvedBody.getText();
|
|
843
|
+
|
|
844
|
+
const deepReauthPatterns = [
|
|
845
|
+
/bcrypt\.compare/i,
|
|
846
|
+
/compareSync/i,
|
|
847
|
+
/verifyPassword/i,
|
|
848
|
+
/checkPassword/i,
|
|
849
|
+
/verify.*2fa/i,
|
|
850
|
+
/totp\.verify/i,
|
|
851
|
+
/speakeasy\.verify/i,
|
|
852
|
+
/authenticator\.verify/i
|
|
853
|
+
];
|
|
854
|
+
|
|
855
|
+
if (deepReauthPatterns.some(pattern => pattern.test(resolvedBodyText))) {
|
|
856
|
+
return true;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
return false;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
tryResolveCalledFunction(callExpr) {
|
|
867
|
+
try {
|
|
868
|
+
const expression = callExpr.getExpression();
|
|
869
|
+
|
|
870
|
+
// Handle property access expressions (e.g., this.authService.verifyPassword)
|
|
871
|
+
if (expression.getKind() === SyntaxKind.PropertyAccessExpression) {
|
|
872
|
+
const nameNode = expression.getNameNode();
|
|
873
|
+
const methodName = nameNode.getText();
|
|
874
|
+
|
|
875
|
+
// Try to find the method definition
|
|
876
|
+
const sourceFile = callExpr.getSourceFile();
|
|
877
|
+
|
|
878
|
+
// Search in the same file
|
|
879
|
+
const methods = sourceFile.getDescendantsOfKind(SyntaxKind.MethodDeclaration);
|
|
880
|
+
for (const method of methods) {
|
|
881
|
+
const name = method.getName();
|
|
882
|
+
if (name === methodName) {
|
|
883
|
+
return method;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// Search for functions
|
|
888
|
+
const functions = sourceFile.getDescendantsOfKind(SyntaxKind.FunctionDeclaration);
|
|
889
|
+
for (const func of functions) {
|
|
890
|
+
const name = func.getName();
|
|
891
|
+
if (name === methodName) {
|
|
892
|
+
return func;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// Try to find in imported files (basic implementation)
|
|
897
|
+
const objectExpr = expression.getExpression();
|
|
898
|
+
if (objectExpr.getKind() === SyntaxKind.PropertyAccessExpression) {
|
|
899
|
+
const serviceName = objectExpr.getNameNode()?.getText();
|
|
900
|
+
if (serviceName) {
|
|
901
|
+
// Look for service injection and try to resolve
|
|
902
|
+
return this.findServiceMethod(sourceFile, serviceName, methodName);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// Handle direct function calls
|
|
908
|
+
if (expression.getKind() === SyntaxKind.Identifier) {
|
|
909
|
+
const functionName = expression.getText();
|
|
910
|
+
const sourceFile = callExpr.getSourceFile();
|
|
911
|
+
|
|
912
|
+
// Search for function declaration
|
|
913
|
+
const functions = sourceFile.getDescendantsOfKind(SyntaxKind.FunctionDeclaration);
|
|
914
|
+
for (const func of functions) {
|
|
915
|
+
if (func.getName() === functionName) {
|
|
916
|
+
return func;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// Search for arrow functions assigned to variables
|
|
921
|
+
const variables = sourceFile.getDescendantsOfKind(SyntaxKind.VariableDeclaration);
|
|
922
|
+
for (const variable of variables) {
|
|
923
|
+
if (variable.getName() === functionName) {
|
|
924
|
+
const initializer = variable.getInitializer();
|
|
925
|
+
if (initializer &&
|
|
926
|
+
(initializer.getKind() === SyntaxKind.ArrowFunction ||
|
|
927
|
+
initializer.getKind() === SyntaxKind.FunctionExpression)) {
|
|
928
|
+
return initializer;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
} catch (error) {
|
|
934
|
+
// Silently fail - resolution is best effort
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
return null;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
findServiceMethod(sourceFile, serviceName, methodName) {
|
|
941
|
+
try {
|
|
942
|
+
// Look for constructor parameters (dependency injection)
|
|
943
|
+
const classes = sourceFile.getDescendantsOfKind(SyntaxKind.ClassDeclaration);
|
|
944
|
+
|
|
945
|
+
for (const classDecl of classes) {
|
|
946
|
+
const constructor = classDecl.getConstructors()[0];
|
|
947
|
+
if (!constructor) continue;
|
|
948
|
+
|
|
949
|
+
const params = constructor.getParameters();
|
|
950
|
+
for (const param of params) {
|
|
951
|
+
const paramName = param.getName();
|
|
952
|
+
|
|
953
|
+
// Check if this is the service we're looking for
|
|
954
|
+
if (paramName === serviceName || paramName.includes(serviceName)) {
|
|
955
|
+
const typeNode = param.getTypeNode();
|
|
956
|
+
if (!typeNode) continue;
|
|
957
|
+
|
|
958
|
+
const typeName = typeNode.getText();
|
|
959
|
+
|
|
960
|
+
// Try to find the service class in the project
|
|
961
|
+
if (this.semanticEngine?.project) {
|
|
962
|
+
const allSourceFiles = this.semanticEngine.project.getSourceFiles();
|
|
963
|
+
|
|
964
|
+
for (const file of allSourceFiles) {
|
|
965
|
+
const serviceClasses = file.getDescendantsOfKind(SyntaxKind.ClassDeclaration);
|
|
966
|
+
|
|
967
|
+
for (const serviceClass of serviceClasses) {
|
|
968
|
+
const className = serviceClass.getName();
|
|
969
|
+
if (className === typeName || typeName.includes(className)) {
|
|
970
|
+
// Found the service class, now find the method
|
|
971
|
+
const methods = serviceClass.getMethods();
|
|
972
|
+
for (const method of methods) {
|
|
973
|
+
if (method.getName() === methodName) {
|
|
974
|
+
return method;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
} catch (error) {
|
|
985
|
+
// Silently fail
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
return null;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
getFunctionName(func) {
|
|
992
|
+
if (func.getKind() === SyntaxKind.FunctionDeclaration ||
|
|
993
|
+
func.getKind() === SyntaxKind.MethodDeclaration) {
|
|
994
|
+
const nameNode = func.getNameNode();
|
|
995
|
+
return nameNode ? nameNode.getText() : null;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// For arrow functions, try to get from parent variable declaration
|
|
999
|
+
const parent = func.getParent();
|
|
1000
|
+
if (parent && parent.getKind() === SyntaxKind.VariableDeclaration) {
|
|
1001
|
+
const nameNode = parent.getNameNode();
|
|
1002
|
+
return nameNode ? nameNode.getText() : null;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
return null;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
findRelevantParent(node) {
|
|
1009
|
+
let parent = node.getParent();
|
|
1010
|
+
let depth = 0;
|
|
1011
|
+
|
|
1012
|
+
while (parent && depth < 5) {
|
|
1013
|
+
const kind = parent.getKind();
|
|
1014
|
+
if (kind === SyntaxKind.FunctionDeclaration ||
|
|
1015
|
+
kind === SyntaxKind.MethodDeclaration ||
|
|
1016
|
+
kind === SyntaxKind.ArrowFunction ||
|
|
1017
|
+
kind === SyntaxKind.Block) {
|
|
1018
|
+
return parent;
|
|
1019
|
+
}
|
|
1020
|
+
parent = parent.getParent();
|
|
1021
|
+
depth++;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
return null;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
hasReauthenticationLogic(node) {
|
|
1028
|
+
const text = node.getText();
|
|
1029
|
+
|
|
1030
|
+
const reauthKeywords = [
|
|
1031
|
+
'reauth',
|
|
1032
|
+
'verifyPassword',
|
|
1033
|
+
'confirmPassword',
|
|
1034
|
+
'requireAuth',
|
|
1035
|
+
'checkPassword',
|
|
1036
|
+
'validatePassword',
|
|
1037
|
+
'2fa',
|
|
1038
|
+
'twoFactor',
|
|
1039
|
+
'requiresReauthForSensitiveActions'
|
|
1040
|
+
];
|
|
1041
|
+
|
|
1042
|
+
return reauthKeywords.some(keyword =>
|
|
1043
|
+
text.toLowerCase().includes(keyword.toLowerCase())
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
hasSessionConfiguration(sourceFile) {
|
|
1048
|
+
const text = sourceFile.getText();
|
|
1049
|
+
return /session\(|express-session|cookie-session|getSessionConfig|sessionConfig/i.test(text);
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
hasIdleTimeoutLogic(sourceFile) {
|
|
1053
|
+
const text = sourceFile.getText();
|
|
1054
|
+
return /idle.*timeout|idleTimeout|inactivity|lastActivity|last.*access|rolling.*true|rolling:\s*true/i.test(text);
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
extractNumericValue(node) {
|
|
1058
|
+
if (!node) return null;
|
|
1059
|
+
|
|
1060
|
+
if (node.getKind() === SyntaxKind.NumericLiteral) {
|
|
1061
|
+
return parseInt(node.getText(), 10);
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// Handle expressions like 1000 * 60 * 60
|
|
1065
|
+
if (node.getKind() === SyntaxKind.BinaryExpression) {
|
|
1066
|
+
try {
|
|
1067
|
+
const text = node.getText();
|
|
1068
|
+
// Simple eval for numeric expressions (be careful in production)
|
|
1069
|
+
const value = Function(`"use strict"; return (${text})`)();
|
|
1070
|
+
return typeof value === 'number' ? value : null;
|
|
1071
|
+
} catch {
|
|
1072
|
+
return null;
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
return null;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
extractBooleanValue(node) {
|
|
1080
|
+
if (!node) return null;
|
|
1081
|
+
|
|
1082
|
+
const kind = node.getKind();
|
|
1083
|
+
if (kind === SyntaxKind.TrueKeyword) return true;
|
|
1084
|
+
if (kind === SyntaxKind.FalseKeyword) return false;
|
|
1085
|
+
|
|
1086
|
+
return null;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
extractTimeValue(node) {
|
|
1090
|
+
if (!node) return null;
|
|
1091
|
+
|
|
1092
|
+
// Handle string literals like "1h", "24h", "30m"
|
|
1093
|
+
if (node.getKind() === SyntaxKind.StringLiteral) {
|
|
1094
|
+
const text = node.getText().replace(/['"]/g, '');
|
|
1095
|
+
return this.parseTimeString(text);
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
// Handle numeric literals (assumed to be milliseconds)
|
|
1099
|
+
return this.extractNumericValue(node);
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
parseTimeString(timeStr) {
|
|
1103
|
+
const match = timeStr.match(/^(\d+)(ms|s|m|h|d|w|y)?$/i);
|
|
1104
|
+
if (!match) return null;
|
|
1105
|
+
|
|
1106
|
+
const value = parseInt(match[1], 10);
|
|
1107
|
+
const unit = (match[2] || 'ms').toLowerCase();
|
|
1108
|
+
|
|
1109
|
+
const multipliers = {
|
|
1110
|
+
'ms': 1,
|
|
1111
|
+
's': 1000,
|
|
1112
|
+
'm': 60 * 1000,
|
|
1113
|
+
'h': 60 * 60 * 1000,
|
|
1114
|
+
'd': 24 * 60 * 60 * 1000,
|
|
1115
|
+
'w': 7 * 24 * 60 * 60 * 1000,
|
|
1116
|
+
'y': 365 * 24 * 60 * 60 * 1000
|
|
1117
|
+
};
|
|
1118
|
+
|
|
1119
|
+
return value * (multipliers[unit] || 1);
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
formatDuration(ms) {
|
|
1123
|
+
const seconds = ms / 1000;
|
|
1124
|
+
const minutes = seconds / 60;
|
|
1125
|
+
const hours = minutes / 60;
|
|
1126
|
+
const days = hours / 24;
|
|
1127
|
+
|
|
1128
|
+
if (days >= 1) return `${days} day${days !== 1 ? 's' : ''}`;
|
|
1129
|
+
if (hours >= 1) return `${hours} hour${hours !== 1 ? 's' : ''}`;
|
|
1130
|
+
if (minutes >= 1) return `${Math.floor(minutes)} minute${Math.floor(minutes) !== 1 ? 's' : ''}`;
|
|
1131
|
+
return `${Math.floor(seconds)} second${Math.floor(seconds) !== 1 ? 's' : ''}`;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
shouldIgnoreFile(filePath) {
|
|
1135
|
+
return this.skipPatterns.some((pattern) => pattern.test(filePath));
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
module.exports = S042SymbolBasedAnalyzer;
|