@sun-asterisk/sunlint 1.3.7 → 1.3.9
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/CHANGELOG.md +63 -0
- package/config/defaults/default.json +2 -1
- package/config/rule-analysis-strategies.js +20 -0
- package/config/rules/enhanced-rules-registry.json +247 -53
- package/core/file-targeting-service.js +98 -7
- package/package.json +1 -1
- package/rules/common/C065_one_behavior_per_test/analyzer.js +851 -0
- package/rules/common/C065_one_behavior_per_test/config.json +95 -0
- package/rules/security/S020_no_eval_dynamic_code/README.md +136 -0
- package/rules/security/S020_no_eval_dynamic_code/analyzer.js +263 -0
- package/rules/security/S020_no_eval_dynamic_code/config.json +54 -0
- package/rules/security/S020_no_eval_dynamic_code/regex-based-analyzer.js +307 -0
- package/rules/security/S020_no_eval_dynamic_code/symbol-based-analyzer.js +280 -0
- package/rules/security/S024_xpath_xxe_protection/symbol-based-analyzer.js +3 -3
- package/rules/security/S025_server_side_validation/symbol-based-analyzer.js +3 -4
- package/rules/security/S030_directory_browsing_protection/README.md +128 -0
- package/rules/security/S030_directory_browsing_protection/analyzer.js +264 -0
- package/rules/security/S030_directory_browsing_protection/config.json +63 -0
- package/rules/security/S030_directory_browsing_protection/regex-based-analyzer.js +483 -0
- package/rules/security/S030_directory_browsing_protection/symbol-based-analyzer.js +539 -0
- package/rules/security/S033_samesite_session_cookies/symbol-based-analyzer.js +8 -9
- package/rules/security/S037_cache_headers/README.md +128 -0
- package/rules/security/S037_cache_headers/analyzer.js +263 -0
- package/rules/security/S037_cache_headers/config.json +50 -0
- package/rules/security/S037_cache_headers/regex-based-analyzer.js +463 -0
- package/rules/security/S037_cache_headers/symbol-based-analyzer.js +546 -0
- package/rules/security/S038_no_version_headers/README.md +234 -0
- package/rules/security/S038_no_version_headers/analyzer.js +262 -0
- package/rules/security/S038_no_version_headers/config.json +49 -0
- package/rules/security/S038_no_version_headers/regex-based-analyzer.js +339 -0
- package/rules/security/S038_no_version_headers/symbol-based-analyzer.js +375 -0
- package/rules/security/S039_no_session_tokens_in_url/README.md +198 -0
- package/rules/security/S039_no_session_tokens_in_url/analyzer.js +262 -0
- package/rules/security/S039_no_session_tokens_in_url/config.json +92 -0
- package/rules/security/S039_no_session_tokens_in_url/regex-based-analyzer.js +337 -0
- package/rules/security/S039_no_session_tokens_in_url/symbol-based-analyzer.js +443 -0
- package/rules/security/S049_short_validity_tokens/analyzer.js +175 -0
- package/rules/security/S049_short_validity_tokens/config.json +124 -0
- package/rules/security/S049_short_validity_tokens/regex-based-analyzer.js +295 -0
- package/rules/security/S049_short_validity_tokens/symbol-based-analyzer.js +389 -0
- package/rules/security/S051_password_length_policy/analyzer.js +410 -0
- package/rules/security/S051_password_length_policy/config.json +83 -0
- package/rules/security/S052_weak_otp_entropy/analyzer.js +403 -0
- package/rules/security/S052_weak_otp_entropy/config.json +57 -0
- package/rules/security/S054_no_default_accounts/README.md +129 -0
- package/rules/security/S054_no_default_accounts/analyzer.js +792 -0
- package/rules/security/S054_no_default_accounts/config.json +101 -0
- package/rules/security/S056_log_injection_protection/analyzer.js +242 -0
- package/rules/security/S056_log_injection_protection/config.json +148 -0
- package/rules/security/S056_log_injection_protection/regex-based-analyzer.js +120 -0
- package/rules/security/S056_log_injection_protection/symbol-based-analyzer.js +246 -0
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S049 Symbol-based Analyzer - Authentication tokens should have short validity periods
|
|
3
|
+
* Detects long-lived tokens using AST analysis
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require("fs");
|
|
7
|
+
const path = require("path");
|
|
8
|
+
|
|
9
|
+
class S049SymbolBasedAnalyzer {
|
|
10
|
+
constructor(semanticEngine = null) {
|
|
11
|
+
this.semanticEngine = semanticEngine;
|
|
12
|
+
this.ruleId = "S049";
|
|
13
|
+
|
|
14
|
+
// Load configuration
|
|
15
|
+
const configPath = path.join(__dirname, 'config.json');
|
|
16
|
+
this.config = JSON.parse(fs.readFileSync(configPath, 'utf8')).configuration;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async initialize(semanticEngine) {
|
|
20
|
+
this.semanticEngine = semanticEngine;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Analyze file for authentication token validity issues
|
|
25
|
+
*/
|
|
26
|
+
async analyze(filePath, language = "typescript", options = {}) {
|
|
27
|
+
if (!this.semanticEngine || !this.semanticEngine.parseCode) {
|
|
28
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
29
|
+
console.log(`🔧 [S049] No semantic engine available or parseCode method missing, skipping symbol-based analysis`);
|
|
30
|
+
}
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const sourceCode = fs.readFileSync(filePath, "utf8");
|
|
36
|
+
const ast = await this.semanticEngine.parseCode(sourceCode, language);
|
|
37
|
+
|
|
38
|
+
if (!ast) {
|
|
39
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
40
|
+
console.log(`🔧 [S049] Failed to parse AST for: ${filePath}`);
|
|
41
|
+
}
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const violations = [];
|
|
46
|
+
|
|
47
|
+
// Traverse AST to find token-related violations
|
|
48
|
+
await this.traverseAST(ast, sourceCode, filePath, violations);
|
|
49
|
+
|
|
50
|
+
return violations;
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error(`🔧 [S049] Error in symbol-based analysis:`, error);
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Traverse AST to find authentication token violations
|
|
59
|
+
*/
|
|
60
|
+
async traverseAST(node, sourceCode, filePath, violations) {
|
|
61
|
+
if (!node || typeof node !== 'object') return;
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
// Check for JWT token creation with long expiration
|
|
65
|
+
if (this.isJWTTokenCreation(node)) {
|
|
66
|
+
this.checkJWTExpiration(node, sourceCode, filePath, violations);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Check for session configuration with long maxAge
|
|
70
|
+
if (this.isSessionConfiguration(node)) {
|
|
71
|
+
this.checkSessionExpiration(node, sourceCode, filePath, violations);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Check for OAuth token configuration
|
|
75
|
+
if (this.isOAuthTokenConfiguration(node)) {
|
|
76
|
+
this.checkOAuthExpiration(node, sourceCode, filePath, violations);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Check for tokens without expiration
|
|
80
|
+
if (this.isTokenWithoutExpiration(node)) {
|
|
81
|
+
this.checkMissingExpiration(node, sourceCode, filePath, violations);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Recursively traverse child nodes
|
|
85
|
+
for (const key in node) {
|
|
86
|
+
if (node[key] && typeof node[key] === 'object') {
|
|
87
|
+
if (Array.isArray(node[key])) {
|
|
88
|
+
for (const child of node[key]) {
|
|
89
|
+
await this.traverseAST(child, sourceCode, filePath, violations);
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
await this.traverseAST(node[key], sourceCode, filePath, violations);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.error(`🔧 [S049] Error traversing AST node:`, error);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Check if node represents JWT token creation
|
|
103
|
+
*/
|
|
104
|
+
isJWTTokenCreation(node) {
|
|
105
|
+
if (node.type === 'CallExpression') {
|
|
106
|
+
const callee = node.callee;
|
|
107
|
+
|
|
108
|
+
// Check for jwt.sign(), jwt.create(), etc.
|
|
109
|
+
if (callee.type === 'MemberExpression') {
|
|
110
|
+
const object = callee.object?.name;
|
|
111
|
+
const method = callee.property?.name;
|
|
112
|
+
|
|
113
|
+
return this.config.jwtLibraries.some(lib => object === lib || object === 'jwt') &&
|
|
114
|
+
this.config.tokenMethods.includes(method);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Check for direct function calls like sign()
|
|
118
|
+
if (callee.type === 'Identifier') {
|
|
119
|
+
return this.config.tokenMethods.includes(callee.name);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Check JWT expiration configuration
|
|
128
|
+
*/
|
|
129
|
+
checkJWTExpiration(node, sourceCode, filePath, violations) {
|
|
130
|
+
try {
|
|
131
|
+
const options = this.getOptionsObject(node);
|
|
132
|
+
if (!options) return;
|
|
133
|
+
|
|
134
|
+
let expirationValue = null;
|
|
135
|
+
let expirationProperty = null;
|
|
136
|
+
|
|
137
|
+
// Find expiration property
|
|
138
|
+
for (const prop of options.properties || []) {
|
|
139
|
+
const key = prop.key?.name || prop.key?.value;
|
|
140
|
+
if (this.config.jwtProperties.includes(key)) {
|
|
141
|
+
expirationProperty = key;
|
|
142
|
+
expirationValue = this.extractValue(prop.value);
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (expirationValue !== null) {
|
|
148
|
+
const seconds = this.parseTimeValue(expirationValue);
|
|
149
|
+
if (seconds > this.config.maxValidityPeriods.accessToken) {
|
|
150
|
+
this.addViolation(violations, node, filePath, sourceCode,
|
|
151
|
+
`JWT token expiration time (${expirationValue}) exceeds recommended maximum of ${this.config.maxValidityPeriods.accessToken} seconds`,
|
|
152
|
+
'long-expiration');
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
// No expiration found
|
|
156
|
+
this.addViolation(violations, node, filePath, sourceCode,
|
|
157
|
+
'JWT token created without expiration time',
|
|
158
|
+
'missing-expiration');
|
|
159
|
+
}
|
|
160
|
+
} catch (error) {
|
|
161
|
+
console.error(`🔧 [S049] Error checking JWT expiration:`, error);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Check if node represents session configuration
|
|
167
|
+
*/
|
|
168
|
+
isSessionConfiguration(node) {
|
|
169
|
+
if (node.type === 'CallExpression') {
|
|
170
|
+
const callee = node.callee;
|
|
171
|
+
|
|
172
|
+
if (callee.type === 'Identifier') {
|
|
173
|
+
return this.config.sessionMethods.includes(callee.name);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (callee.type === 'MemberExpression') {
|
|
177
|
+
const method = callee.property?.name;
|
|
178
|
+
return this.config.sessionMethods.includes(method);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Check session expiration configuration
|
|
187
|
+
*/
|
|
188
|
+
checkSessionExpiration(node, sourceCode, filePath, violations) {
|
|
189
|
+
try {
|
|
190
|
+
const options = this.getOptionsObject(node);
|
|
191
|
+
if (!options) return;
|
|
192
|
+
|
|
193
|
+
let maxAge = null;
|
|
194
|
+
|
|
195
|
+
// Find maxAge property
|
|
196
|
+
for (const prop of options.properties || []) {
|
|
197
|
+
const key = prop.key?.name || prop.key?.value;
|
|
198
|
+
if (key === 'maxAge' || key === 'expires') {
|
|
199
|
+
maxAge = this.extractValue(prop.value);
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (maxAge !== null) {
|
|
205
|
+
const seconds = this.parseTimeValue(maxAge, true); // Session times often in milliseconds
|
|
206
|
+
if (seconds > this.config.maxValidityPeriods.sessionToken) {
|
|
207
|
+
this.addViolation(violations, node, filePath, sourceCode,
|
|
208
|
+
`Session maxAge (${maxAge}) exceeds recommended maximum of ${this.config.maxValidityPeriods.sessionToken} seconds`,
|
|
209
|
+
'long-session');
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
} catch (error) {
|
|
213
|
+
console.error(`🔧 [S049] Error checking session expiration:`, error);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Check if node represents OAuth token configuration
|
|
219
|
+
*/
|
|
220
|
+
isOAuthTokenConfiguration(node) {
|
|
221
|
+
if (node.type === 'ObjectExpression') {
|
|
222
|
+
return node.properties?.some(prop => {
|
|
223
|
+
const key = prop.key?.name || prop.key?.value;
|
|
224
|
+
return this.config.oauthProperties.includes(key);
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Check OAuth token expiration
|
|
233
|
+
*/
|
|
234
|
+
checkOAuthExpiration(node, sourceCode, filePath, violations) {
|
|
235
|
+
try {
|
|
236
|
+
for (const prop of node.properties || []) {
|
|
237
|
+
const key = prop.key?.name || prop.key?.value;
|
|
238
|
+
if (this.config.oauthProperties.includes(key)) {
|
|
239
|
+
const value = this.extractValue(prop.value);
|
|
240
|
+
if (value !== null) {
|
|
241
|
+
const seconds = this.parseTimeValue(value);
|
|
242
|
+
if (seconds > this.config.maxValidityPeriods.accessToken) {
|
|
243
|
+
this.addViolation(violations, node, filePath, sourceCode,
|
|
244
|
+
`OAuth token lifetime (${value}) exceeds recommended maximum of ${this.config.maxValidityPeriods.accessToken} seconds`,
|
|
245
|
+
'long-oauth-token');
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
} catch (error) {
|
|
251
|
+
console.error(`🔧 [S049] Error checking OAuth expiration:`, error);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Check if token is created without expiration
|
|
257
|
+
*/
|
|
258
|
+
isTokenWithoutExpiration(node) {
|
|
259
|
+
if (node.type === 'CallExpression' && this.isJWTTokenCreation(node)) {
|
|
260
|
+
const options = this.getOptionsObject(node);
|
|
261
|
+
if (!options) return true; // No options object means no expiration
|
|
262
|
+
|
|
263
|
+
// Check if any expiration property exists
|
|
264
|
+
return !options.properties?.some(prop => {
|
|
265
|
+
const key = prop.key?.name || prop.key?.value;
|
|
266
|
+
return this.config.jwtProperties.includes(key);
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Check for missing expiration
|
|
275
|
+
*/
|
|
276
|
+
checkMissingExpiration(node, sourceCode, filePath, violations) {
|
|
277
|
+
this.addViolation(violations, node, filePath, sourceCode,
|
|
278
|
+
'Authentication token created without expiration time',
|
|
279
|
+
'missing-expiration');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Get options object from function call
|
|
284
|
+
*/
|
|
285
|
+
getOptionsObject(node) {
|
|
286
|
+
if (!node.arguments || node.arguments.length < 3) return null;
|
|
287
|
+
|
|
288
|
+
const optionsArg = node.arguments[2]; // Usually the third argument
|
|
289
|
+
if (optionsArg?.type === 'ObjectExpression') {
|
|
290
|
+
return optionsArg;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Extract value from AST node
|
|
298
|
+
*/
|
|
299
|
+
extractValue(node) {
|
|
300
|
+
if (!node) return null;
|
|
301
|
+
|
|
302
|
+
switch (node.type) {
|
|
303
|
+
case 'Literal':
|
|
304
|
+
return node.value;
|
|
305
|
+
case 'TemplateLiteral':
|
|
306
|
+
if (node.expressions.length === 0 && node.quasis.length === 1) {
|
|
307
|
+
return node.quasis[0].value.raw;
|
|
308
|
+
}
|
|
309
|
+
break;
|
|
310
|
+
case 'BinaryExpression':
|
|
311
|
+
if (node.operator === '*') {
|
|
312
|
+
const left = this.extractValue(node.left);
|
|
313
|
+
const right = this.extractValue(node.right);
|
|
314
|
+
if (typeof left === 'number' && typeof right === 'number') {
|
|
315
|
+
return left * right;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Parse time value to seconds
|
|
326
|
+
*/
|
|
327
|
+
parseTimeValue(value, isMilliseconds = false) {
|
|
328
|
+
if (typeof value === 'number') {
|
|
329
|
+
return isMilliseconds ? Math.floor(value / 1000) : value;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (typeof value === 'string') {
|
|
333
|
+
// Handle string formats like '1h', '30d', '2w'
|
|
334
|
+
const match = value.match(/^(\d+)\s*([a-zA-Z]+)?$/);
|
|
335
|
+
if (match) {
|
|
336
|
+
const num = parseInt(match[1]);
|
|
337
|
+
const unit = match[2]?.toLowerCase() || 's';
|
|
338
|
+
|
|
339
|
+
const multiplier = this.config.timeUnits[unit] ||
|
|
340
|
+
this.config.timeUnits[unit + 's'] || 1;
|
|
341
|
+
|
|
342
|
+
return num * multiplier;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return value || 0;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Add violation to results
|
|
351
|
+
*/
|
|
352
|
+
addViolation(violations, node, filePath, sourceCode, message, subType) {
|
|
353
|
+
const lines = sourceCode.split('\n');
|
|
354
|
+
const startLine = node.loc?.start?.line || 1;
|
|
355
|
+
const endLine = node.loc?.end?.line || startLine;
|
|
356
|
+
|
|
357
|
+
violations.push({
|
|
358
|
+
ruleId: this.ruleId,
|
|
359
|
+
message,
|
|
360
|
+
severity: "error",
|
|
361
|
+
line: startLine,
|
|
362
|
+
column: node.loc?.start?.column || 0,
|
|
363
|
+
endLine,
|
|
364
|
+
endColumn: node.loc?.end?.column || 0,
|
|
365
|
+
source: lines[startLine - 1] || "",
|
|
366
|
+
filePath,
|
|
367
|
+
type: "symbol-based",
|
|
368
|
+
subType,
|
|
369
|
+
context: {
|
|
370
|
+
surrounding: this.getSurroundingLines(lines, startLine, 3)
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Get surrounding lines for context
|
|
377
|
+
*/
|
|
378
|
+
getSurroundingLines(lines, centerLine, contextLines) {
|
|
379
|
+
const start = Math.max(0, centerLine - contextLines - 1);
|
|
380
|
+
const end = Math.min(lines.length, centerLine + contextLines);
|
|
381
|
+
return lines.slice(start, end).join('\n');
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
cleanup() {
|
|
385
|
+
// Cleanup resources if needed
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
module.exports = S049SymbolBasedAnalyzer;
|