@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.
Files changed (51) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/config/defaults/default.json +2 -1
  3. package/config/rule-analysis-strategies.js +20 -0
  4. package/config/rules/enhanced-rules-registry.json +247 -53
  5. package/core/file-targeting-service.js +98 -7
  6. package/package.json +1 -1
  7. package/rules/common/C065_one_behavior_per_test/analyzer.js +851 -0
  8. package/rules/common/C065_one_behavior_per_test/config.json +95 -0
  9. package/rules/security/S020_no_eval_dynamic_code/README.md +136 -0
  10. package/rules/security/S020_no_eval_dynamic_code/analyzer.js +263 -0
  11. package/rules/security/S020_no_eval_dynamic_code/config.json +54 -0
  12. package/rules/security/S020_no_eval_dynamic_code/regex-based-analyzer.js +307 -0
  13. package/rules/security/S020_no_eval_dynamic_code/symbol-based-analyzer.js +280 -0
  14. package/rules/security/S024_xpath_xxe_protection/symbol-based-analyzer.js +3 -3
  15. package/rules/security/S025_server_side_validation/symbol-based-analyzer.js +3 -4
  16. package/rules/security/S030_directory_browsing_protection/README.md +128 -0
  17. package/rules/security/S030_directory_browsing_protection/analyzer.js +264 -0
  18. package/rules/security/S030_directory_browsing_protection/config.json +63 -0
  19. package/rules/security/S030_directory_browsing_protection/regex-based-analyzer.js +483 -0
  20. package/rules/security/S030_directory_browsing_protection/symbol-based-analyzer.js +539 -0
  21. package/rules/security/S033_samesite_session_cookies/symbol-based-analyzer.js +8 -9
  22. package/rules/security/S037_cache_headers/README.md +128 -0
  23. package/rules/security/S037_cache_headers/analyzer.js +263 -0
  24. package/rules/security/S037_cache_headers/config.json +50 -0
  25. package/rules/security/S037_cache_headers/regex-based-analyzer.js +463 -0
  26. package/rules/security/S037_cache_headers/symbol-based-analyzer.js +546 -0
  27. package/rules/security/S038_no_version_headers/README.md +234 -0
  28. package/rules/security/S038_no_version_headers/analyzer.js +262 -0
  29. package/rules/security/S038_no_version_headers/config.json +49 -0
  30. package/rules/security/S038_no_version_headers/regex-based-analyzer.js +339 -0
  31. package/rules/security/S038_no_version_headers/symbol-based-analyzer.js +375 -0
  32. package/rules/security/S039_no_session_tokens_in_url/README.md +198 -0
  33. package/rules/security/S039_no_session_tokens_in_url/analyzer.js +262 -0
  34. package/rules/security/S039_no_session_tokens_in_url/config.json +92 -0
  35. package/rules/security/S039_no_session_tokens_in_url/regex-based-analyzer.js +337 -0
  36. package/rules/security/S039_no_session_tokens_in_url/symbol-based-analyzer.js +443 -0
  37. package/rules/security/S049_short_validity_tokens/analyzer.js +175 -0
  38. package/rules/security/S049_short_validity_tokens/config.json +124 -0
  39. package/rules/security/S049_short_validity_tokens/regex-based-analyzer.js +295 -0
  40. package/rules/security/S049_short_validity_tokens/symbol-based-analyzer.js +389 -0
  41. package/rules/security/S051_password_length_policy/analyzer.js +410 -0
  42. package/rules/security/S051_password_length_policy/config.json +83 -0
  43. package/rules/security/S052_weak_otp_entropy/analyzer.js +403 -0
  44. package/rules/security/S052_weak_otp_entropy/config.json +57 -0
  45. package/rules/security/S054_no_default_accounts/README.md +129 -0
  46. package/rules/security/S054_no_default_accounts/analyzer.js +792 -0
  47. package/rules/security/S054_no_default_accounts/config.json +101 -0
  48. package/rules/security/S056_log_injection_protection/analyzer.js +242 -0
  49. package/rules/security/S056_log_injection_protection/config.json +148 -0
  50. package/rules/security/S056_log_injection_protection/regex-based-analyzer.js +120 -0
  51. 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;