@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,345 @@
|
|
|
1
|
+
# S045 - Brute-force Protection
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
This rule enforces protection against brute-force attacks on authentication endpoints. It detects missing rate limiting, account lockout mechanisms, and other brute-force protection measures in authentication flows to prevent unauthorized access attempts.
|
|
6
|
+
|
|
7
|
+
## Description
|
|
8
|
+
|
|
9
|
+
Brute-force protection is a critical security measure that helps prevent:
|
|
10
|
+
|
|
11
|
+
- **Brute-force attacks** on login and authentication endpoints
|
|
12
|
+
- **Credential stuffing** attacks using stolen credentials
|
|
13
|
+
- **Automated attacks** that attempt to guess passwords
|
|
14
|
+
- **Account takeover** through systematic password attempts
|
|
15
|
+
- **System overload** from excessive authentication requests
|
|
16
|
+
|
|
17
|
+
## Rule Details
|
|
18
|
+
|
|
19
|
+
**Rule ID**: S045
|
|
20
|
+
**Category**: Security
|
|
21
|
+
**Severity**: Error
|
|
22
|
+
**Type**: Hybrid Analysis (Symbol-based + Regex-based)
|
|
23
|
+
|
|
24
|
+
## Examples
|
|
25
|
+
|
|
26
|
+
### ❌ Violations
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
// NestJS - Login endpoint without rate limiting
|
|
30
|
+
@Post('login')
|
|
31
|
+
async login(@Body() loginDto: LoginDto) {
|
|
32
|
+
return this.authService.validateUser(loginDto);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Express.js - Authentication without rate limiting
|
|
36
|
+
app.post('/auth/login', (req, res) => {
|
|
37
|
+
const { username, password } = req.body;
|
|
38
|
+
// No rate limiting or lockout mechanism
|
|
39
|
+
authenticateUser(username, password);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Password reset without protection
|
|
43
|
+
@Post('reset-password')
|
|
44
|
+
async resetPassword(@Body() resetDto: ResetPasswordDto) {
|
|
45
|
+
// No rate limiting or captcha
|
|
46
|
+
return this.authService.resetPassword(resetDto);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Sign-in endpoint without throttling
|
|
50
|
+
app.post('/signin', async (req, res) => {
|
|
51
|
+
const { email, password } = req.body;
|
|
52
|
+
// Missing brute-force protection
|
|
53
|
+
const user = await User.findOne({ email });
|
|
54
|
+
if (user && await bcrypt.compare(password, user.password)) {
|
|
55
|
+
res.json({ token: generateToken(user) });
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Authentication handler without lockout
|
|
60
|
+
function authenticateUser(username, password) {
|
|
61
|
+
// No attempt counting or lockout mechanism
|
|
62
|
+
return validateCredentials(username, password);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Forgot password without protection
|
|
66
|
+
@Post('forgot-password')
|
|
67
|
+
async forgotPassword(@Body() body: { email: string }) {
|
|
68
|
+
// No rate limiting for password reset requests
|
|
69
|
+
return this.authService.sendResetEmail(body.email);
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### ✅ Correct Usage
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
// NestJS - Login with rate limiting
|
|
77
|
+
@Post('login')
|
|
78
|
+
@Throttle(5, 60) // 5 attempts per minute
|
|
79
|
+
async login(@Body() loginDto: LoginDto) {
|
|
80
|
+
return this.authService.validateUser(loginDto);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Express.js - Authentication with rate limiting middleware
|
|
84
|
+
const rateLimit = require('express-rate-limit');
|
|
85
|
+
|
|
86
|
+
const loginLimiter = rateLimit({
|
|
87
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
88
|
+
max: 5, // limit each IP to 5 requests per windowMs
|
|
89
|
+
message: 'Too many login attempts, please try again later'
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
app.post('/auth/login', loginLimiter, (req, res) => {
|
|
93
|
+
const { username, password } = req.body;
|
|
94
|
+
authenticateUser(username, password);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// NestJS with ThrottlerModule configuration
|
|
98
|
+
@Module({
|
|
99
|
+
imports: [
|
|
100
|
+
ThrottlerModule.forRoot([{
|
|
101
|
+
ttl: 60000, // 1 minute
|
|
102
|
+
limit: 5, // 5 requests per minute
|
|
103
|
+
}]),
|
|
104
|
+
],
|
|
105
|
+
})
|
|
106
|
+
export class AuthModule {}
|
|
107
|
+
|
|
108
|
+
// Express with rate-limiter-flexible
|
|
109
|
+
const { RateLimiterMemory } = require('rate-limiter-flexible');
|
|
110
|
+
|
|
111
|
+
const rateLimiter = new RateLimiterMemory({
|
|
112
|
+
points: 5, // Number of attempts
|
|
113
|
+
duration: 900, // Per 15 minutes
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
app.post('/login', async (req, res) => {
|
|
117
|
+
try {
|
|
118
|
+
await rateLimiter.consume(req.ip);
|
|
119
|
+
// Process login
|
|
120
|
+
} catch (rejRes) {
|
|
121
|
+
res.status(429).send('Too Many Requests');
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Password reset with rate limiting and captcha
|
|
126
|
+
@Post('reset-password')
|
|
127
|
+
@Throttle(3, 3600) // 3 attempts per hour
|
|
128
|
+
async resetPassword(
|
|
129
|
+
@Body() resetDto: ResetPasswordDto,
|
|
130
|
+
@Body('captcha') captcha: string
|
|
131
|
+
) {
|
|
132
|
+
// Verify captcha first
|
|
133
|
+
await this.captchaService.verify(captcha);
|
|
134
|
+
return this.authService.resetPassword(resetDto);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Account lockout implementation
|
|
138
|
+
const accountLockout = new Map();
|
|
139
|
+
|
|
140
|
+
app.post('/login', async (req, res) => {
|
|
141
|
+
const { username, password } = req.body;
|
|
142
|
+
const attempts = accountLockout.get(username) || 0;
|
|
143
|
+
|
|
144
|
+
if (attempts >= 5) {
|
|
145
|
+
return res.status(423).json({
|
|
146
|
+
error: 'Account locked due to too many failed attempts'
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const isValid = await validateCredentials(username, password);
|
|
151
|
+
if (!isValid) {
|
|
152
|
+
accountLockout.set(username, attempts + 1);
|
|
153
|
+
return res.status(401).json({ error: 'Invalid credentials' });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Reset attempts on successful login
|
|
157
|
+
accountLockout.delete(username);
|
|
158
|
+
res.json({ token: generateToken(username) });
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Express Brute for progressive delays
|
|
162
|
+
const ExpressBrute = require('express-brute');
|
|
163
|
+
const store = new ExpressBrute.MemoryStore();
|
|
164
|
+
|
|
165
|
+
const bruteforce = new ExpressBrute(store, {
|
|
166
|
+
freeRetries: 3,
|
|
167
|
+
minWait: 5 * 60 * 1000, // 5 minutes
|
|
168
|
+
maxWait: 60 * 60 * 1000, // 1 hour
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
app.post('/auth/login', bruteforce.prevent, (req, res) => {
|
|
172
|
+
// Login logic with brute-force protection
|
|
173
|
+
});
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Configuration
|
|
177
|
+
|
|
178
|
+
### Detected Patterns
|
|
179
|
+
|
|
180
|
+
This rule detects brute-force protection issues in multiple scenarios:
|
|
181
|
+
|
|
182
|
+
### Authentication Endpoints
|
|
183
|
+
|
|
184
|
+
- `login`, `signin`, `authenticate`, `auth` endpoints without protection
|
|
185
|
+
- `password`, `reset`, `forgot` endpoints without rate limiting
|
|
186
|
+
- Authentication handlers without throttling mechanisms
|
|
187
|
+
|
|
188
|
+
### Rate Limiting Libraries
|
|
189
|
+
|
|
190
|
+
**Supported Libraries:**
|
|
191
|
+
- `express-rate-limit`
|
|
192
|
+
- `express-slow-down`
|
|
193
|
+
- `@nestjs/throttler`
|
|
194
|
+
- `rate-limiter-flexible`
|
|
195
|
+
- `bottleneck`
|
|
196
|
+
- `limiter`
|
|
197
|
+
|
|
198
|
+
### Account Lockout Libraries
|
|
199
|
+
|
|
200
|
+
**Supported Libraries:**
|
|
201
|
+
- `express-slow-down`
|
|
202
|
+
- `rate-limiter-flexible`
|
|
203
|
+
- `express-brute`
|
|
204
|
+
- `express-brute-mongo`
|
|
205
|
+
|
|
206
|
+
### CAPTCHA Libraries
|
|
207
|
+
|
|
208
|
+
**Supported Libraries:**
|
|
209
|
+
- `recaptcha`
|
|
210
|
+
- `hcaptcha`
|
|
211
|
+
- `turnstile`
|
|
212
|
+
- `captcha`
|
|
213
|
+
|
|
214
|
+
### Protection Patterns
|
|
215
|
+
|
|
216
|
+
**Required Protection Indicators:**
|
|
217
|
+
- Rate limiting: `rate.*limit`, `throttle`, `@Throttle()`
|
|
218
|
+
- Account lockout: `lockout`, `max.*attempts`, `brute.*force.*protection`
|
|
219
|
+
- Progressive delays: `cooldown`, `progressive.*delay`
|
|
220
|
+
- CAPTCHA verification: `captcha`, `recaptcha`, `hcaptcha`
|
|
221
|
+
|
|
222
|
+
### Vulnerable Patterns
|
|
223
|
+
|
|
224
|
+
**Detected Anti-patterns:**
|
|
225
|
+
- Authentication endpoints without rate limiting
|
|
226
|
+
- Login handlers without throttling
|
|
227
|
+
- Password reset without protection
|
|
228
|
+
- Sign-in without attempt limits
|
|
229
|
+
|
|
230
|
+
## Security Benefits
|
|
231
|
+
|
|
232
|
+
1. **Attack Prevention**: Blocks automated brute-force attacks
|
|
233
|
+
2. **Resource Protection**: Prevents system overload from excessive requests
|
|
234
|
+
3. **Account Security**: Protects user accounts from unauthorized access
|
|
235
|
+
4. **Service Availability**: Maintains service availability under attack
|
|
236
|
+
5. **Compliance**: Meets security standards and regulations
|
|
237
|
+
|
|
238
|
+
## Analysis Approach
|
|
239
|
+
|
|
240
|
+
### Symbol-based Analysis (Primary)
|
|
241
|
+
|
|
242
|
+
- Uses TypeScript AST for semantic analysis
|
|
243
|
+
- Detects authentication endpoints and decorators
|
|
244
|
+
- Analyzes middleware usage and rate limiting
|
|
245
|
+
- Provides precise line/column positions
|
|
246
|
+
|
|
247
|
+
### Regex-based Analysis (Fallback)
|
|
248
|
+
|
|
249
|
+
- Pattern-based detection for complex cases
|
|
250
|
+
- Handles string-based endpoint definitions
|
|
251
|
+
- Covers edge cases missed by AST analysis
|
|
252
|
+
- Maintains line number accuracy
|
|
253
|
+
|
|
254
|
+
## Best Practices
|
|
255
|
+
|
|
256
|
+
1. **Implement rate limiting** on all authentication endpoints
|
|
257
|
+
2. **Use progressive delays** for repeated failed attempts
|
|
258
|
+
3. **Enable account lockout** after multiple failed attempts
|
|
259
|
+
4. **Add CAPTCHA verification** for sensitive operations
|
|
260
|
+
5. **Monitor and log** authentication attempts
|
|
261
|
+
6. **Use IP-based rate limiting** to prevent distributed attacks
|
|
262
|
+
7. **Implement proper error messages** that don't reveal user existence
|
|
263
|
+
8. **Set appropriate time windows** for rate limiting (15-60 minutes)
|
|
264
|
+
9. **Configure reasonable attempt limits** (3-5 attempts before lockout)
|
|
265
|
+
10. **Use established libraries** rather than custom implementations
|
|
266
|
+
|
|
267
|
+
## Configuration Options
|
|
268
|
+
|
|
269
|
+
### Default Settings
|
|
270
|
+
|
|
271
|
+
- **Max Attempts Threshold**: 5 attempts
|
|
272
|
+
- **Time Window**: 15 minutes
|
|
273
|
+
- **Enable Rate Limit Detection**: true
|
|
274
|
+
- **Enable Account Lockout Detection**: true
|
|
275
|
+
- **Enable CAPTCHA Detection**: true
|
|
276
|
+
|
|
277
|
+
### Customizable Parameters
|
|
278
|
+
|
|
279
|
+
```json
|
|
280
|
+
{
|
|
281
|
+
"maxAttemptsThreshold": 5,
|
|
282
|
+
"timeWindowMinutes": 15,
|
|
283
|
+
"checkAuthenticationEndpoints": [
|
|
284
|
+
"login", "signin", "authenticate", "auth",
|
|
285
|
+
"password", "reset", "forgot"
|
|
286
|
+
]
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
## Testing
|
|
291
|
+
|
|
292
|
+
Run the rule on test fixtures:
|
|
293
|
+
|
|
294
|
+
```bash
|
|
295
|
+
# Test violations
|
|
296
|
+
node cli.js --rule=S045 --input=examples/rule-test-fixtures/rules/S045_brute_force_protection/violations --engine=heuristic
|
|
297
|
+
|
|
298
|
+
# Test clean examples
|
|
299
|
+
node cli.js --rule=S045 --input=examples/rule-test-fixtures/rules/S045_brute_force_protection/clean --engine=heuristic
|
|
300
|
+
|
|
301
|
+
# Test all examples
|
|
302
|
+
node cli.js --rule=S045 --input=examples/rule-test-fixtures/rules/S045_brute_force_protection --engine=heuristic
|
|
303
|
+
|
|
304
|
+
# Framework-specific testing
|
|
305
|
+
# Test with ESLint engine (fast)
|
|
306
|
+
node cli.js --rule=S045 --input=path/to/your/files
|
|
307
|
+
|
|
308
|
+
# Test with heuristic engine (comprehensive)
|
|
309
|
+
node cli.js --rule=S045 --input=path/to/your/files --engine=heuristic
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
## Framework Compatibility
|
|
313
|
+
|
|
314
|
+
- **Node.js**: All versions
|
|
315
|
+
- **Frameworks**: Express.js, NestJS, Fastify, Koa
|
|
316
|
+
- **Languages**: JavaScript, TypeScript
|
|
317
|
+
- **Analysis Engines**: ESLint (fast), Heuristic (comprehensive)
|
|
318
|
+
|
|
319
|
+
## Related Rules
|
|
320
|
+
|
|
321
|
+
- **S031**: Set Secure flag for Session Cookies
|
|
322
|
+
- **S032**: Set HttpOnly attribute for Session Cookies
|
|
323
|
+
- **S041**: Session Tokens must be invalidated after logout or expiration
|
|
324
|
+
- **S044**: Re-authentication Required for Sensitive Operations
|
|
325
|
+
- **S048**: No Current Password in Password Reset
|
|
326
|
+
|
|
327
|
+
Together, these rules provide comprehensive authentication security.
|
|
328
|
+
|
|
329
|
+
## OWASP References
|
|
330
|
+
|
|
331
|
+
- [OWASP Brute Force Attack](https://owasp.org/www-community/attacks/Brute_force_attack)
|
|
332
|
+
- [OWASP Authentication Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html)
|
|
333
|
+
- [OWASP Blocking Brute Force Attacks](https://owasp.org/www-community/controls/Blocking_Brute_Force_Attacks)
|
|
334
|
+
- [PortSwigger Web Security - Brute Force](https://portswigger.net/web-security/authentication/password-based/brute-force)
|
|
335
|
+
|
|
336
|
+
## Implementation Notes
|
|
337
|
+
|
|
338
|
+
This rule uses a hybrid analysis approach:
|
|
339
|
+
|
|
340
|
+
1. **Symbol-based analysis** detects decorator usage (`@Throttle()`) and middleware patterns
|
|
341
|
+
2. **Regex-based analysis** catches string-based configurations and complex patterns
|
|
342
|
+
3. **Library detection** identifies known rate limiting and protection libraries
|
|
343
|
+
4. **Context awareness** distinguishes between authentication and other endpoints
|
|
344
|
+
|
|
345
|
+
The rule maintains high accuracy while minimizing false positives through careful pattern matching and context analysis.
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S045 Main Analyzer - Brute-force Protection
|
|
3
|
+
* Primary: Symbol-based analysis (when available)
|
|
4
|
+
* Fallback: Regex-based for all other cases
|
|
5
|
+
* Command: node cli.js --rule=S045 --input=examples/rule-test-fixtures/rules/S045_brute_force_protection --engine=heuristic
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const S045SymbolBasedAnalyzer = require("./symbol-based-analyzer.js");
|
|
9
|
+
|
|
10
|
+
class S045Analyzer {
|
|
11
|
+
constructor(options = {}) {
|
|
12
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
13
|
+
console.log(`🔧 [S045] Constructor called with options:`, !!options);
|
|
14
|
+
console.log(
|
|
15
|
+
`🔧 [S045] Options type:`,
|
|
16
|
+
typeof options,
|
|
17
|
+
Object.keys(options || {})
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
this.ruleId = "S045";
|
|
22
|
+
this.ruleName = "Brute-force Protection";
|
|
23
|
+
this.description =
|
|
24
|
+
"Implement protection against brute-force attacks on authentication endpoints. This rule detects missing rate limiting, account lockout mechanisms, and other brute-force protection measures in authentication flows.";
|
|
25
|
+
this.semanticEngine = options.semanticEngine || null;
|
|
26
|
+
this.verbose = options.verbose || false;
|
|
27
|
+
|
|
28
|
+
// Configuration
|
|
29
|
+
this.config = {
|
|
30
|
+
useSymbolBased: true, // Primary approach
|
|
31
|
+
fallbackToRegex: true, // Secondary approach
|
|
32
|
+
regexBasedOnly: false, // Can be set to true for pure mode
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Initialize analyzers
|
|
36
|
+
try {
|
|
37
|
+
this.symbolAnalyzer = new S045SymbolBasedAnalyzer(this.semanticEngine);
|
|
38
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
39
|
+
console.log(`🔧 [S045] Symbol analyzer created successfully`);
|
|
40
|
+
}
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.error(`🔧 [S045] Error creating symbol analyzer:`, error);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Initialize analyzer with semantic engine
|
|
48
|
+
*/
|
|
49
|
+
async initialize(semanticEngine) {
|
|
50
|
+
this.semanticEngine = semanticEngine;
|
|
51
|
+
|
|
52
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
53
|
+
console.log(`🔧 [S045] Main analyzer initializing...`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Initialize symbol analyzer
|
|
57
|
+
if (this.symbolAnalyzer) {
|
|
58
|
+
await this.symbolAnalyzer.initialize?.(semanticEngine);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
62
|
+
console.log(`🔧 [S045] Main analyzer initialized successfully`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Single file analysis method for testing
|
|
68
|
+
*/
|
|
69
|
+
analyzeSingle(filePath, options = {}) {
|
|
70
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
71
|
+
console.log(`📊 [S045] analyzeSingle() called for: ${filePath}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Return result using same format as analyze method
|
|
75
|
+
return this.analyze([filePath], "typescript", options);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async analyze(files, language, options = {}) {
|
|
79
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
80
|
+
console.log(
|
|
81
|
+
`🔧 [S045] analyze() method called with ${files.length} files, language: ${language}`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const violations = [];
|
|
86
|
+
|
|
87
|
+
for (const filePath of files) {
|
|
88
|
+
try {
|
|
89
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
90
|
+
console.log(`🔧 [S045] Processing file: ${filePath}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const fileViolations = await this.analyzeFile(filePath, options);
|
|
94
|
+
violations.push(...fileViolations);
|
|
95
|
+
|
|
96
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
97
|
+
console.log(
|
|
98
|
+
`🔧 [S045] File ${filePath}: Found ${fileViolations.length} violations`
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
} catch (error) {
|
|
102
|
+
console.warn(
|
|
103
|
+
`⚠ [S045] Analysis failed for ${filePath}:`,
|
|
104
|
+
error.message
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
110
|
+
console.log(`🔧 [S045] Total violations found: ${violations.length}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return violations;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async analyzeFile(filePath, options = {}) {
|
|
117
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
118
|
+
console.log(`🔍 [S045] analyzeFile() called for: ${filePath}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Create a Map to track unique violations and prevent duplicates
|
|
122
|
+
const violationMap = new Map();
|
|
123
|
+
|
|
124
|
+
// 1. Try Symbol-based analysis first (primary)
|
|
125
|
+
if (
|
|
126
|
+
this.config.useSymbolBased &&
|
|
127
|
+
this.semanticEngine?.project &&
|
|
128
|
+
this.semanticEngine?.initialized
|
|
129
|
+
) {
|
|
130
|
+
try {
|
|
131
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
132
|
+
console.log(`🔧 [S045] Trying symbol-based analysis...`);
|
|
133
|
+
}
|
|
134
|
+
const sourceFile = this.semanticEngine.project.getSourceFile(filePath);
|
|
135
|
+
if (sourceFile) {
|
|
136
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
137
|
+
console.log(`🔧 [S045] Source file found, analyzing...`);
|
|
138
|
+
}
|
|
139
|
+
const symbolViolations = await this.symbolAnalyzer.analyze(
|
|
140
|
+
sourceFile,
|
|
141
|
+
filePath
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
// Add to violation map with deduplication
|
|
145
|
+
symbolViolations.forEach((violation) => {
|
|
146
|
+
const key = `${violation.line}:${violation.column}:${violation.message}`;
|
|
147
|
+
if (!violationMap.has(key)) {
|
|
148
|
+
violationMap.set(key, violation);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
153
|
+
console.log(
|
|
154
|
+
`🔧 [S045] Symbol analysis completed: ${symbolViolations.length} violations`
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
} else {
|
|
158
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
159
|
+
console.log(`🔧 [S045] Source file not found, falling back...`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
} catch (error) {
|
|
163
|
+
console.warn(`⚠ [S045] Symbol analysis failed:`, error.message);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// 2. Fallback to regex-based analysis if needed
|
|
168
|
+
if (this.config.fallbackToRegex || this.config.regexBasedOnly) {
|
|
169
|
+
try {
|
|
170
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
171
|
+
console.log(`🔧 [S045] Trying regex-based analysis...`);
|
|
172
|
+
}
|
|
173
|
+
const regexViolations = await this.analyzeWithRegex(filePath);
|
|
174
|
+
|
|
175
|
+
// Add to violation map with deduplication
|
|
176
|
+
regexViolations.forEach((violation) => {
|
|
177
|
+
const key = `${violation.line}:${violation.column}:${violation.message}`;
|
|
178
|
+
if (!violationMap.has(key)) {
|
|
179
|
+
violationMap.set(key, violation);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
184
|
+
console.log(
|
|
185
|
+
`🔧 [S045] Regex analysis completed: ${regexViolations.length} violations`
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
} catch (error) {
|
|
189
|
+
console.warn(`⚠ [S045] Regex analysis failed:`, error.message);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Convert Map values to array and add filePath to each violation
|
|
194
|
+
const finalViolations = Array.from(violationMap.values()).map(
|
|
195
|
+
(violation) => ({
|
|
196
|
+
...violation,
|
|
197
|
+
filePath: filePath,
|
|
198
|
+
file: filePath, // Also add 'file' for compatibility
|
|
199
|
+
})
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
203
|
+
console.log(
|
|
204
|
+
`🔧 [S045] File analysis completed: ${finalViolations.length} unique violations`
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return finalViolations;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async analyzeWithRegex(filePath) {
|
|
212
|
+
const fs = require('fs');
|
|
213
|
+
const violations = [];
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
217
|
+
const lines = content.split('\n');
|
|
218
|
+
|
|
219
|
+
// Authentication endpoint patterns - simplified and more specific
|
|
220
|
+
const authEndpointPatterns = [
|
|
221
|
+
{ pattern: /@Post\(['"`](.*login.*)['"`]\)/gi, type: 'nestjs' },
|
|
222
|
+
{ pattern: /@Post\(['"`](.*signin.*)['"`]\)/gi, type: 'nestjs' },
|
|
223
|
+
{ pattern: /@Post\(['"`](.*auth.*)['"`]\)/gi, type: 'nestjs' },
|
|
224
|
+
{ pattern: /@Post\(['"`](.*password.*)['"`]\)/gi, type: 'nestjs' },
|
|
225
|
+
{ pattern: /@Post\(['"`](.*reset.*)['"`]\)/gi, type: 'nestjs' },
|
|
226
|
+
{ pattern: /@Post\(['"`](.*signup.*)['"`]\)/gi, type: 'nestjs' },
|
|
227
|
+
{ pattern: /app\.post\(['"`](.*login.*)['"`]/gi, type: 'express' },
|
|
228
|
+
{ pattern: /app\.post\(['"`](.*signin.*)['"`]/gi, type: 'express' },
|
|
229
|
+
{ pattern: /app\.post\(['"`](.*auth.*)['"`]/gi, type: 'express' },
|
|
230
|
+
{ pattern: /app\.post\(['"`](.*password.*)['"`]/gi, type: 'express' },
|
|
231
|
+
{ pattern: /app\.post\(['"`](.*reset.*)['"`]/gi, type: 'express' },
|
|
232
|
+
{ pattern: /app\.post\(['"`](.*signup.*)['"`]/gi, type: 'express' },
|
|
233
|
+
{ pattern: /router\.post\(['"`](.*login.*)['"`]/gi, type: 'express' },
|
|
234
|
+
{ pattern: /router\.post\(['"`](.*signin.*)['"`]/gi, type: 'express' },
|
|
235
|
+
{ pattern: /router\.post\(['"`](.*auth.*)['"`]/gi, type: 'express' },
|
|
236
|
+
{ pattern: /router\.post\(['"`](.*password.*)['"`]/gi, type: 'express' },
|
|
237
|
+
{ pattern: /router\.post\(['"`](.*reset.*)['"`]/gi, type: 'express' },
|
|
238
|
+
{ pattern: /router\.post\(['"`](.*signup.*)['"`]/gi, type: 'express' }
|
|
239
|
+
];
|
|
240
|
+
|
|
241
|
+
// Protection patterns - more comprehensive
|
|
242
|
+
const protectionPatterns = [
|
|
243
|
+
/@Throttle/gi,
|
|
244
|
+
/@RateLimit/gi,
|
|
245
|
+
/@UseGuards.*ThrottlerGuard/gi,
|
|
246
|
+
/rateLimit/gi,
|
|
247
|
+
/throttle/gi,
|
|
248
|
+
/express-rate-limit/gi,
|
|
249
|
+
/@nestjs\/throttler/gi,
|
|
250
|
+
/lockout/gi,
|
|
251
|
+
/maxAttempts/gi,
|
|
252
|
+
/cooldown/gi,
|
|
253
|
+
/brute.*force.*protection/gi,
|
|
254
|
+
/windowMs/gi,
|
|
255
|
+
/max.*requests/gi,
|
|
256
|
+
/express-slow-down/gi,
|
|
257
|
+
/rate-limiter-flexible/gi,
|
|
258
|
+
/Limiter/gi,
|
|
259
|
+
/limiter/gi,
|
|
260
|
+
/RateLimit/gi,
|
|
261
|
+
/Throttle/gi
|
|
262
|
+
];
|
|
263
|
+
|
|
264
|
+
// Check if file has any protection mechanisms at all
|
|
265
|
+
const hasGlobalProtection = protectionPatterns.some(pattern => {
|
|
266
|
+
pattern.lastIndex = 0;
|
|
267
|
+
return pattern.test(content);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
for (let i = 0; i < lines.length; i++) {
|
|
271
|
+
const line = lines[i];
|
|
272
|
+
const lineNumber = i + 1;
|
|
273
|
+
|
|
274
|
+
// Check for authentication endpoints
|
|
275
|
+
for (const { pattern, type } of authEndpointPatterns) {
|
|
276
|
+
// Reset regex lastIndex to avoid issues with global flag
|
|
277
|
+
pattern.lastIndex = 0;
|
|
278
|
+
const match = pattern.exec(line);
|
|
279
|
+
if (match) {
|
|
280
|
+
// Check if this specific endpoint has protection
|
|
281
|
+
const hasProtection = this.checkProtectionInContext(lines, i, protectionPatterns);
|
|
282
|
+
|
|
283
|
+
// Always flag violations if no protection is found
|
|
284
|
+
if (!hasProtection) {
|
|
285
|
+
violations.push({
|
|
286
|
+
rule: this.ruleId,
|
|
287
|
+
source: filePath,
|
|
288
|
+
category: "security",
|
|
289
|
+
line: lineNumber,
|
|
290
|
+
column: 1,
|
|
291
|
+
message: `Authentication endpoint '${match[1]}' lacks brute-force protection (rate limiting, account lockout, or CAPTCHA)`,
|
|
292
|
+
severity: "error",
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return violations;
|
|
300
|
+
} catch (error) {
|
|
301
|
+
if (this.verbose) {
|
|
302
|
+
console.log(`🔍 [${this.ruleId}] Regex analysis error:`, error.message);
|
|
303
|
+
}
|
|
304
|
+
return [];
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
checkProtectionInContext(lines, currentLineIndex, protectionPatterns) {
|
|
309
|
+
// Check current line and surrounding context (10 lines before and after)
|
|
310
|
+
const start = Math.max(0, currentLineIndex - 10);
|
|
311
|
+
const end = Math.min(lines.length, currentLineIndex + 11);
|
|
312
|
+
|
|
313
|
+
for (let i = start; i < end; i++) {
|
|
314
|
+
const line = lines[i];
|
|
315
|
+
for (const pattern of protectionPatterns) {
|
|
316
|
+
pattern.lastIndex = 0;
|
|
317
|
+
if (pattern.test(line)) {
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Clean up resources
|
|
328
|
+
*/
|
|
329
|
+
cleanup() {
|
|
330
|
+
if (this.symbolAnalyzer?.cleanup) {
|
|
331
|
+
this.symbolAnalyzer.cleanup();
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
module.exports = S045Analyzer;
|