@sun-asterisk/sunlint 1.3.18 → 1.3.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config/rules/enhanced-rules-registry.json +77 -18
- package/core/cli-program.js +9 -1
- package/core/github-annotate-service.js +986 -0
- package/core/output-service.js +294 -6
- package/core/summary-report-service.js +30 -30
- package/docs/GITHUB_ACTIONS_INTEGRATION.md +421 -0
- package/package.json +2 -1
- package/rules/common/C014_dependency_injection/symbol-based-analyzer.js +392 -280
- package/rules/common/C017_constructor_logic/analyzer.js +137 -503
- package/rules/common/C017_constructor_logic/config.json +50 -0
- package/rules/common/C017_constructor_logic/symbol-based-analyzer.js +463 -0
- package/rules/security/S006_no_plaintext_recovery_codes/symbol-based-analyzer.js +463 -21
- package/rules/security/S011_secure_guid_generation/README.md +255 -0
- package/rules/security/S011_secure_guid_generation/analyzer.js +135 -0
- package/rules/security/S011_secure_guid_generation/config.json +56 -0
- package/rules/security/S011_secure_guid_generation/symbol-based-analyzer.js +609 -0
- package/rules/security/S028_file_upload_size_limits/README.md +537 -0
- package/rules/security/S028_file_upload_size_limits/analyzer.js +202 -0
- package/rules/security/S028_file_upload_size_limits/config.json +186 -0
- package/rules/security/S028_file_upload_size_limits/symbol-based-analyzer.js +530 -0
- package/rules/security/S041_session_token_invalidation/README.md +303 -0
- package/rules/security/S041_session_token_invalidation/analyzer.js +242 -0
- package/rules/security/S041_session_token_invalidation/config.json +175 -0
- package/rules/security/S041_session_token_invalidation/regex-based-analyzer.js +411 -0
- package/rules/security/S041_session_token_invalidation/symbol-based-analyzer.js +674 -0
- package/rules/security/S044_re_authentication_required/README.md +136 -0
- package/rules/security/S044_re_authentication_required/analyzer.js +242 -0
- package/rules/security/S044_re_authentication_required/config.json +161 -0
- package/rules/security/S044_re_authentication_required/regex-based-analyzer.js +329 -0
- package/rules/security/S044_re_authentication_required/symbol-based-analyzer.js +537 -0
- package/rules/security/S045_brute_force_protection/README.md +345 -0
- package/rules/security/S045_brute_force_protection/analyzer.js +336 -0
- package/rules/security/S045_brute_force_protection/config.json +139 -0
- package/rules/security/S045_brute_force_protection/symbol-based-analyzer.js +646 -0
- package/rules/common/C017_constructor_logic/semantic-analyzer.js +0 -340
|
@@ -0,0 +1,646 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S045 Symbol-Based Analyzer - Brute-force Protection
|
|
3
|
+
* Uses TypeScript compiler API for semantic analysis
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const ts = require("typescript");
|
|
7
|
+
|
|
8
|
+
class S045SymbolBasedAnalyzer {
|
|
9
|
+
constructor(semanticEngine = null) {
|
|
10
|
+
this.semanticEngine = semanticEngine;
|
|
11
|
+
this.ruleId = "S045";
|
|
12
|
+
this.category = "security";
|
|
13
|
+
|
|
14
|
+
// Authentication endpoint patterns
|
|
15
|
+
this.authEndpoints = [
|
|
16
|
+
"login",
|
|
17
|
+
"signin",
|
|
18
|
+
"authenticate",
|
|
19
|
+
"auth",
|
|
20
|
+
"password",
|
|
21
|
+
"reset",
|
|
22
|
+
"forgot",
|
|
23
|
+
"signup",
|
|
24
|
+
"register"
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
// Rate limiting libraries and decorators
|
|
28
|
+
this.rateLimitLibraries = [
|
|
29
|
+
"express-rate-limit",
|
|
30
|
+
"express-slow-down",
|
|
31
|
+
"@nestjs/throttler",
|
|
32
|
+
"rate-limiter-flexible",
|
|
33
|
+
"bottleneck",
|
|
34
|
+
"limiter"
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
// Account lockout libraries
|
|
38
|
+
this.lockoutLibraries = [
|
|
39
|
+
"express-slow-down",
|
|
40
|
+
"rate-limiter-flexible",
|
|
41
|
+
"express-brute",
|
|
42
|
+
"express-brute-mongo"
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
// CAPTCHA libraries
|
|
46
|
+
this.captchaLibraries = [
|
|
47
|
+
"recaptcha",
|
|
48
|
+
"hcaptcha",
|
|
49
|
+
"turnstile",
|
|
50
|
+
"captcha"
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
// Protection decorators and middleware
|
|
54
|
+
this.protectionDecorators = [
|
|
55
|
+
"Throttle",
|
|
56
|
+
"RateLimit",
|
|
57
|
+
"ThrottleGuard",
|
|
58
|
+
"RateLimitGuard"
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
// Protection patterns in code
|
|
62
|
+
this.protectionPatterns = [
|
|
63
|
+
"rate.*limit",
|
|
64
|
+
"throttle",
|
|
65
|
+
"lockout",
|
|
66
|
+
"captcha",
|
|
67
|
+
"brute.*force.*protection",
|
|
68
|
+
"max.*attempts",
|
|
69
|
+
"cooldown",
|
|
70
|
+
"windowMs",
|
|
71
|
+
"max.*requests"
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
// Vulnerable patterns that indicate missing protection
|
|
75
|
+
this.vulnerablePatterns = [
|
|
76
|
+
"login.*without.*rate.*limit",
|
|
77
|
+
"auth.*without.*throttle",
|
|
78
|
+
"password.*without.*lockout",
|
|
79
|
+
"signin.*without.*captcha"
|
|
80
|
+
];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Initialize analyzer with semantic engine
|
|
85
|
+
*/
|
|
86
|
+
async initialize(semanticEngine) {
|
|
87
|
+
this.semanticEngine = semanticEngine;
|
|
88
|
+
if (this.verbose) {
|
|
89
|
+
console.log(`🔍 [${this.ruleId}] Symbol: Semantic engine initialized`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async analyze(filePath) {
|
|
94
|
+
if (this.verbose) {
|
|
95
|
+
console.log(
|
|
96
|
+
`🔍 [${this.ruleId}] Symbol: Starting analysis for ${filePath}`
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!this.semanticEngine) {
|
|
101
|
+
if (this.verbose) {
|
|
102
|
+
console.log(
|
|
103
|
+
`🔍 [${this.ruleId}] Symbol: No semantic engine available, skipping`
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const sourceFile = this.semanticEngine.getSourceFile(filePath);
|
|
111
|
+
console.log('sourceFile', sourceFile);
|
|
112
|
+
if (!sourceFile) {
|
|
113
|
+
if (this.verbose) {
|
|
114
|
+
console.log(
|
|
115
|
+
`🔍 [${this.ruleId}] Symbol: No source file found, trying ts-morph fallback`
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
return await this.analyzeTsMorph(filePath);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (this.verbose) {
|
|
122
|
+
console.log(`🔧 [${this.ruleId}] Source file found, analyzing...`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return await this.analyzeSourceFile(sourceFile, filePath);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
if (this.verbose) {
|
|
128
|
+
console.log(
|
|
129
|
+
`🔍 [${this.ruleId}] Symbol: Error in analysis:`,
|
|
130
|
+
error.message
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
return [];
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async analyzeTsMorph(filePath) {
|
|
138
|
+
try {
|
|
139
|
+
if (this.verbose) {
|
|
140
|
+
console.log(`🔍 [${this.ruleId}] Symbol: Starting ts-morph analysis`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const { Project } = require("ts-morph");
|
|
144
|
+
const project = new Project();
|
|
145
|
+
const sourceFile = project.addSourceFileAtPath(filePath);
|
|
146
|
+
|
|
147
|
+
return await this.analyzeSourceFile(sourceFile, filePath);
|
|
148
|
+
} catch (error) {
|
|
149
|
+
if (this.verbose) {
|
|
150
|
+
console.log(
|
|
151
|
+
`🔍 [${this.ruleId}] Symbol: ts-morph analysis failed:`,
|
|
152
|
+
error.message
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
return [];
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async analyzeSourceFile(sourceFile, filePath) {
|
|
160
|
+
const violations = [];
|
|
161
|
+
const startTime = Date.now();
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
if (this.verbose) {
|
|
165
|
+
console.log(`🔍 [${this.ruleId}] Symbol: Starting symbol-based analysis for ${filePath}`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Find all method declarations and decorators
|
|
169
|
+
const methodDeclarations = sourceFile.getDescendantsOfKind
|
|
170
|
+
? sourceFile.getDescendantsOfKind(
|
|
171
|
+
require("typescript").SyntaxKind.MethodDeclaration
|
|
172
|
+
)
|
|
173
|
+
: [];
|
|
174
|
+
|
|
175
|
+
const functionDeclarations = sourceFile.getDescendantsOfKind
|
|
176
|
+
? sourceFile.getDescendantsOfKind(
|
|
177
|
+
require("typescript").SyntaxKind.FunctionDeclaration
|
|
178
|
+
)
|
|
179
|
+
: [];
|
|
180
|
+
|
|
181
|
+
const arrowFunctions = sourceFile.getDescendantsOfKind
|
|
182
|
+
? sourceFile.getDescendantsOfKind(
|
|
183
|
+
require("typescript").SyntaxKind.ArrowFunction
|
|
184
|
+
)
|
|
185
|
+
: [];
|
|
186
|
+
|
|
187
|
+
const allFunctions = [
|
|
188
|
+
...methodDeclarations,
|
|
189
|
+
...functionDeclarations,
|
|
190
|
+
...arrowFunctions
|
|
191
|
+
];
|
|
192
|
+
|
|
193
|
+
if (this.verbose) {
|
|
194
|
+
console.log(
|
|
195
|
+
`🔍 [${this.ruleId}] Symbol: Found ${allFunctions.length} functions (${methodDeclarations.length} methods, ${functionDeclarations.length} functions, ${arrowFunctions.length} arrow functions)`
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
let authChecked = 0;
|
|
200
|
+
let rateLimitChecked = 0;
|
|
201
|
+
let lockoutChecked = 0;
|
|
202
|
+
let funcErrors = 0;
|
|
203
|
+
|
|
204
|
+
for (const func of allFunctions) {
|
|
205
|
+
try {
|
|
206
|
+
const funcName = this.getFunctionName(func);
|
|
207
|
+
|
|
208
|
+
if (this.verbose) {
|
|
209
|
+
console.log(`🔍 [${this.ruleId}] Symbol: Analyzing function '${funcName}'`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Check for authentication endpoints without protection
|
|
213
|
+
const authViolation = this.analyzeAuthenticationEndpoint(func, sourceFile);
|
|
214
|
+
authChecked++;
|
|
215
|
+
if (authViolation) {
|
|
216
|
+
violations.push(authViolation);
|
|
217
|
+
if (this.verbose) {
|
|
218
|
+
console.log(`⚠️ [${this.ruleId}] Symbol: Auth violation found in '${funcName}'`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Check for missing rate limiting
|
|
223
|
+
const rateLimitViolation = this.analyzeRateLimiting(func, sourceFile);
|
|
224
|
+
rateLimitChecked++;
|
|
225
|
+
if (rateLimitViolation) {
|
|
226
|
+
violations.push(rateLimitViolation);
|
|
227
|
+
if (this.verbose) {
|
|
228
|
+
console.log(`⚠️ [${this.ruleId}] Symbol: Rate limit violation found in '${funcName}'`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Check for missing account lockout
|
|
233
|
+
const lockoutViolation = this.analyzeAccountLockout(func, sourceFile);
|
|
234
|
+
lockoutChecked++;
|
|
235
|
+
if (lockoutViolation) {
|
|
236
|
+
violations.push(lockoutViolation);
|
|
237
|
+
if (this.verbose) {
|
|
238
|
+
console.log(`⚠️ [${this.ruleId}] Symbol: Account lockout violation found in '${funcName}'`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
} catch (error) {
|
|
243
|
+
funcErrors++;
|
|
244
|
+
if (this.verbose) {
|
|
245
|
+
console.log(
|
|
246
|
+
`🔍 [${this.ruleId}] Symbol: Error analyzing function:`,
|
|
247
|
+
error.message
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Check for missing protection in route definitions
|
|
254
|
+
const callExpressions = sourceFile.getDescendantsOfKind
|
|
255
|
+
? sourceFile.getDescendantsOfKind(
|
|
256
|
+
require("typescript").SyntaxKind.CallExpression
|
|
257
|
+
)
|
|
258
|
+
: [];
|
|
259
|
+
|
|
260
|
+
if (this.verbose) {
|
|
261
|
+
console.log(`🔍 [${this.ruleId}] Symbol: Found ${callExpressions.length} call expressions to analyze`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
let routeChecked = 0;
|
|
265
|
+
let routeErrors = 0;
|
|
266
|
+
|
|
267
|
+
for (const callNode of callExpressions) {
|
|
268
|
+
try {
|
|
269
|
+
const routeViolation = this.analyzeRouteProtection(callNode, sourceFile);
|
|
270
|
+
routeChecked++;
|
|
271
|
+
if (routeViolation) {
|
|
272
|
+
violations.push(routeViolation);
|
|
273
|
+
if (this.verbose) {
|
|
274
|
+
console.log(`⚠️ [${this.ruleId}] Symbol: Route protection violation found`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
} catch (error) {
|
|
278
|
+
routeErrors++;
|
|
279
|
+
if (this.verbose) {
|
|
280
|
+
console.log(
|
|
281
|
+
`🔍 [${this.ruleId}] Symbol: Error analyzing call expression:`,
|
|
282
|
+
error.message
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const duration = Date.now() - startTime;
|
|
289
|
+
|
|
290
|
+
if (this.verbose) {
|
|
291
|
+
console.log(
|
|
292
|
+
`✅ [${this.ruleId}] Symbol: Analysis completed in ${duration}ms. ` +
|
|
293
|
+
`Found ${violations.length} violations from ${allFunctions.length} functions and ${callExpressions.length} call expressions. ` +
|
|
294
|
+
`Stats: auth=${authChecked}, rateLimit=${rateLimitChecked}, lockout=${lockoutChecked}, routes=${routeChecked}, ` +
|
|
295
|
+
`errors=${funcErrors + routeErrors}`
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return violations;
|
|
300
|
+
} catch (error) {
|
|
301
|
+
if (this.verbose) {
|
|
302
|
+
console.log(
|
|
303
|
+
`🔍 [${this.ruleId}] Symbol: Error in source file analysis:`,
|
|
304
|
+
error.message
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
return [];
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
analyzeAuthenticationEndpoint(func, sourceFile) {
|
|
312
|
+
try {
|
|
313
|
+
const funcName = this.getFunctionName(func);
|
|
314
|
+
const funcText = func.getText();
|
|
315
|
+
|
|
316
|
+
// Check if this is an authentication endpoint
|
|
317
|
+
const isAuthEndpoint = this.authEndpoints.some(endpoint =>
|
|
318
|
+
funcName.toLowerCase().includes(endpoint) ||
|
|
319
|
+
funcText.toLowerCase().includes(`/${endpoint}`)
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
if (!isAuthEndpoint) {
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (this.verbose) {
|
|
327
|
+
console.log(
|
|
328
|
+
`🔍 [${this.ruleId}] Symbol: Authentication endpoint detected: ${funcName}`
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Check if protection mechanisms are present
|
|
333
|
+
const hasProtection = this.hasProtectionMechanisms(func, sourceFile);
|
|
334
|
+
if (!hasProtection) {
|
|
335
|
+
return this.createViolation(
|
|
336
|
+
sourceFile,
|
|
337
|
+
func,
|
|
338
|
+
`Authentication endpoint '${funcName}' lacks brute-force protection (rate limiting, account lockout, or CAPTCHA)`
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return null;
|
|
343
|
+
} catch (error) {
|
|
344
|
+
if (this.verbose) {
|
|
345
|
+
console.log(
|
|
346
|
+
`🔍 [${this.ruleId}] Symbol: Error analyzing auth endpoint:`,
|
|
347
|
+
error.message
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
analyzeRateLimiting(func, sourceFile) {
|
|
355
|
+
try {
|
|
356
|
+
const funcName = this.getFunctionName(func);
|
|
357
|
+
const funcText = func.getText();
|
|
358
|
+
|
|
359
|
+
// Check if this is a login/auth function
|
|
360
|
+
const isAuthFunction = this.authEndpoints.some(endpoint =>
|
|
361
|
+
funcName.toLowerCase().includes(endpoint)
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
if (!isAuthFunction) {
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Check for rate limiting decorators or middleware
|
|
369
|
+
const hasRateLimit = this.hasRateLimiting(func, sourceFile);
|
|
370
|
+
if (!hasRateLimit) {
|
|
371
|
+
return this.createViolation(
|
|
372
|
+
sourceFile,
|
|
373
|
+
func,
|
|
374
|
+
`Authentication function '${funcName}' missing rate limiting protection`
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return null;
|
|
379
|
+
} catch (error) {
|
|
380
|
+
if (this.verbose) {
|
|
381
|
+
console.log(
|
|
382
|
+
`🔍 [${this.ruleId}] Symbol: Error analyzing rate limiting:`,
|
|
383
|
+
error.message
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
analyzeAccountLockout(func, sourceFile) {
|
|
391
|
+
try {
|
|
392
|
+
const funcName = this.getFunctionName(func);
|
|
393
|
+
const funcText = func.getText();
|
|
394
|
+
|
|
395
|
+
// Check if this is a login/auth function
|
|
396
|
+
const isAuthFunction = this.authEndpoints.some(endpoint =>
|
|
397
|
+
funcName.toLowerCase().includes(endpoint)
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
if (!isAuthFunction) {
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Check for account lockout mechanisms
|
|
405
|
+
const hasLockout = this.hasAccountLockout(func, sourceFile);
|
|
406
|
+
if (!hasLockout) {
|
|
407
|
+
return this.createViolation(
|
|
408
|
+
sourceFile,
|
|
409
|
+
func,
|
|
410
|
+
`Authentication function '${funcName}' missing account lockout protection`
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return null;
|
|
415
|
+
} catch (error) {
|
|
416
|
+
if (this.verbose) {
|
|
417
|
+
console.log(
|
|
418
|
+
`🔍 [${this.ruleId}] Symbol: Error analyzing account lockout:`,
|
|
419
|
+
error.message
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
analyzeRouteProtection(callNode, sourceFile) {
|
|
427
|
+
try {
|
|
428
|
+
const expression = callNode.getExpression();
|
|
429
|
+
const methodName = this.getMethodName(expression);
|
|
430
|
+
|
|
431
|
+
// Check for route definitions (app.post, router.get, etc.)
|
|
432
|
+
const routeMethods = ["post", "get", "put", "delete", "patch"];
|
|
433
|
+
if (!routeMethods.includes(methodName.toLowerCase())) {
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const args = callNode.getArguments();
|
|
438
|
+
if (args.length === 0) {
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const routePath = args[0].getText();
|
|
443
|
+
const isAuthRoute = this.authEndpoints.some(endpoint =>
|
|
444
|
+
routePath.toLowerCase().includes(`/${endpoint}`) ||
|
|
445
|
+
routePath.toLowerCase().includes(`"${endpoint}"`) ||
|
|
446
|
+
routePath.toLowerCase().includes(`'${endpoint}'`)
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
if (!isAuthRoute) {
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Check if route has protection middleware
|
|
454
|
+
const hasProtection = this.hasRouteProtection(callNode, sourceFile);
|
|
455
|
+
if (!hasProtection) {
|
|
456
|
+
return this.createViolation(
|
|
457
|
+
sourceFile,
|
|
458
|
+
callNode,
|
|
459
|
+
`Authentication route ${routePath} missing brute-force protection middleware`
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return null;
|
|
464
|
+
} catch (error) {
|
|
465
|
+
if (this.verbose) {
|
|
466
|
+
console.log(
|
|
467
|
+
`🔍 [${this.ruleId}] Symbol: Error analyzing route protection:`,
|
|
468
|
+
error.message
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
return null;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
hasProtectionMechanisms(func, sourceFile) {
|
|
476
|
+
try {
|
|
477
|
+
const funcText = func.getText();
|
|
478
|
+
|
|
479
|
+
// Check for decorators
|
|
480
|
+
const decorators = func.getDecorators ? func.getDecorators() : [];
|
|
481
|
+
const hasProtectionDecorator = decorators.some(decorator => {
|
|
482
|
+
const decoratorText = decorator.getText();
|
|
483
|
+
return this.protectionDecorators.some(pattern =>
|
|
484
|
+
decoratorText.includes(pattern)
|
|
485
|
+
);
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
if (hasProtectionDecorator) {
|
|
489
|
+
return true;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Check for protection patterns in function body
|
|
493
|
+
const hasProtectionPattern = this.protectionPatterns.some(pattern => {
|
|
494
|
+
const regex = new RegExp(pattern, 'i');
|
|
495
|
+
return regex.test(funcText);
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
return hasProtectionPattern;
|
|
499
|
+
} catch (error) {
|
|
500
|
+
return false;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
hasRateLimiting(func, sourceFile) {
|
|
505
|
+
try {
|
|
506
|
+
const funcText = func.getText();
|
|
507
|
+
|
|
508
|
+
// Check for rate limiting decorators
|
|
509
|
+
const decorators = func.getDecorators ? func.getDecorators() : [];
|
|
510
|
+
const hasRateLimitDecorator = decorators.some(decorator => {
|
|
511
|
+
const decoratorText = decorator.getText();
|
|
512
|
+
return decoratorText.includes('Throttle') ||
|
|
513
|
+
decoratorText.includes('RateLimit') ||
|
|
514
|
+
decoratorText.includes('ThrottleGuard');
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
if (hasRateLimitDecorator) {
|
|
518
|
+
return true;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Check for rate limiting patterns
|
|
522
|
+
const rateLimitPatterns = [
|
|
523
|
+
/rate.*limit/i,
|
|
524
|
+
/throttle/i,
|
|
525
|
+
/windowMs/i,
|
|
526
|
+
/max.*requests/i,
|
|
527
|
+
/express-rate-limit/i,
|
|
528
|
+
/@nestjs\/throttler/i
|
|
529
|
+
];
|
|
530
|
+
|
|
531
|
+
return rateLimitPatterns.some(pattern => pattern.test(funcText));
|
|
532
|
+
} catch (error) {
|
|
533
|
+
return false;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
hasAccountLockout(func, sourceFile) {
|
|
538
|
+
try {
|
|
539
|
+
const funcText = func.getText();
|
|
540
|
+
|
|
541
|
+
// Check for lockout patterns
|
|
542
|
+
const lockoutPatterns = [
|
|
543
|
+
/lockout/i,
|
|
544
|
+
/max.*attempts/i,
|
|
545
|
+
/cooldown/i,
|
|
546
|
+
/account.*lock/i,
|
|
547
|
+
/brute.*force.*protection/i,
|
|
548
|
+
/express-brute/i,
|
|
549
|
+
/rate-limiter-flexible/i
|
|
550
|
+
];
|
|
551
|
+
|
|
552
|
+
return lockoutPatterns.some(pattern => pattern.test(funcText));
|
|
553
|
+
} catch (error) {
|
|
554
|
+
return false;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
hasRouteProtection(callNode, sourceFile) {
|
|
559
|
+
try {
|
|
560
|
+
// Check if there are middleware functions before the handler
|
|
561
|
+
const parent = callNode.getParent();
|
|
562
|
+
if (!parent) {
|
|
563
|
+
return false;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const parentText = parent.getText();
|
|
567
|
+
|
|
568
|
+
// Look for middleware patterns
|
|
569
|
+
const middlewarePatterns = [
|
|
570
|
+
/rateLimit/i,
|
|
571
|
+
/throttle/i,
|
|
572
|
+
/lockout/i,
|
|
573
|
+
/brute.*force/i,
|
|
574
|
+
/express-rate-limit/i,
|
|
575
|
+
/express-slow-down/i
|
|
576
|
+
];
|
|
577
|
+
|
|
578
|
+
return middlewarePatterns.some(pattern => pattern.test(parentText));
|
|
579
|
+
} catch (error) {
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
getFunctionName(func) {
|
|
585
|
+
try {
|
|
586
|
+
if (func.getName) {
|
|
587
|
+
return func.getName();
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// For arrow functions or anonymous functions
|
|
591
|
+
const parent = func.getParent();
|
|
592
|
+
if (parent && parent.getName) {
|
|
593
|
+
return parent.getName();
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
return "anonymous";
|
|
597
|
+
} catch (error) {
|
|
598
|
+
return "unknown";
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
getMethodName(expression) {
|
|
603
|
+
try {
|
|
604
|
+
const ts = require("typescript");
|
|
605
|
+
|
|
606
|
+
if (expression.getKind() === ts.SyntaxKind.PropertyAccessExpression) {
|
|
607
|
+
return expression.getNameNode().getText();
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
if (expression.getKind() === ts.SyntaxKind.Identifier) {
|
|
611
|
+
return expression.getText();
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
return "";
|
|
615
|
+
} catch (error) {
|
|
616
|
+
return "";
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
createViolation(sourceFile, node, message) {
|
|
621
|
+
try {
|
|
622
|
+
const start = node.getStart();
|
|
623
|
+
const lineAndChar = sourceFile.getLineAndColumnAtPos(start);
|
|
624
|
+
|
|
625
|
+
return {
|
|
626
|
+
rule: this.ruleId,
|
|
627
|
+
source: sourceFile.getFilePath(),
|
|
628
|
+
category: this.category,
|
|
629
|
+
line: lineAndChar.line,
|
|
630
|
+
column: lineAndChar.column,
|
|
631
|
+
message: message,
|
|
632
|
+
severity: "error",
|
|
633
|
+
};
|
|
634
|
+
} catch (error) {
|
|
635
|
+
if (this.verbose) {
|
|
636
|
+
console.log(
|
|
637
|
+
`🔍 [${this.ruleId}] Symbol: Error creating violation:`,
|
|
638
|
+
error.message
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
return null;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
module.exports = S045SymbolBasedAnalyzer;
|