@sun-asterisk/sunlint 1.3.7 → 1.3.8

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 (38) hide show
  1. package/CHANGELOG.md +38 -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 +190 -35
  5. package/core/file-targeting-service.js +83 -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/S037_cache_headers/README.md +128 -0
  10. package/rules/security/S037_cache_headers/analyzer.js +263 -0
  11. package/rules/security/S037_cache_headers/config.json +50 -0
  12. package/rules/security/S037_cache_headers/regex-based-analyzer.js +463 -0
  13. package/rules/security/S037_cache_headers/symbol-based-analyzer.js +546 -0
  14. package/rules/security/S038_no_version_headers/README.md +234 -0
  15. package/rules/security/S038_no_version_headers/analyzer.js +262 -0
  16. package/rules/security/S038_no_version_headers/config.json +49 -0
  17. package/rules/security/S038_no_version_headers/regex-based-analyzer.js +339 -0
  18. package/rules/security/S038_no_version_headers/symbol-based-analyzer.js +375 -0
  19. package/rules/security/S039_no_session_tokens_in_url/README.md +198 -0
  20. package/rules/security/S039_no_session_tokens_in_url/analyzer.js +262 -0
  21. package/rules/security/S039_no_session_tokens_in_url/config.json +92 -0
  22. package/rules/security/S039_no_session_tokens_in_url/regex-based-analyzer.js +337 -0
  23. package/rules/security/S039_no_session_tokens_in_url/symbol-based-analyzer.js +436 -0
  24. package/rules/security/S049_short_validity_tokens/analyzer.js +175 -0
  25. package/rules/security/S049_short_validity_tokens/config.json +124 -0
  26. package/rules/security/S049_short_validity_tokens/regex-based-analyzer.js +295 -0
  27. package/rules/security/S049_short_validity_tokens/symbol-based-analyzer.js +389 -0
  28. package/rules/security/S051_password_length_policy/analyzer.js +410 -0
  29. package/rules/security/S051_password_length_policy/config.json +83 -0
  30. package/rules/security/S052_weak_otp_entropy/analyzer.js +403 -0
  31. package/rules/security/S052_weak_otp_entropy/config.json +57 -0
  32. package/rules/security/S054_no_default_accounts/README.md +129 -0
  33. package/rules/security/S054_no_default_accounts/analyzer.js +792 -0
  34. package/rules/security/S054_no_default_accounts/config.json +101 -0
  35. package/rules/security/S056_log_injection_protection/analyzer.js +242 -0
  36. package/rules/security/S056_log_injection_protection/config.json +148 -0
  37. package/rules/security/S056_log_injection_protection/regex-based-analyzer.js +120 -0
  38. package/rules/security/S056_log_injection_protection/symbol-based-analyzer.js +287 -0
@@ -0,0 +1,124 @@
1
+ {
2
+ "rule": {
3
+ "id": "S049",
4
+ "name": "Authentication tokens should have short validity periods",
5
+ "description": "Authentication tokens (JWT, session tokens, etc.) should have appropriately short validity periods to minimize the risk of token compromise. Long-lived tokens increase the attack surface and potential impact of token theft.",
6
+ "category": "security",
7
+ "severity": "error",
8
+ "languages": ["typescript", "javascript"],
9
+ "frameworks": ["express", "nestjs", "node"],
10
+ "version": "1.0.0",
11
+ "status": "stable",
12
+ "tags": ["security", "authentication", "tokens", "jwt", "session", "owasp"],
13
+ "references": [
14
+ "https://owasp.org/www-project-web-security-testing-guide/v42/4-Web_Application_Security_Testing/04-Authentication_Testing/01-Testing_for_Credentials_Transported_over_an_Encrypted_Channel",
15
+ "https://owasp.org/www-project-application-security-verification-standard/",
16
+ "https://cheatsheetseries.owasp.org/cheatsheets/JSON_Web_Token_for_Java_Cheat_Sheet.html",
17
+ "https://tools.ietf.org/html/rfc7519#section-4.1.4"
18
+ ]
19
+ },
20
+ "configuration": {
21
+ "enableJWTValidityCheck": true,
22
+ "enableSessionTokenCheck": true,
23
+ "enableOAuthTokenCheck": true,
24
+ "maxValidityPeriods": {
25
+ "accessToken": 3600,
26
+ "refreshToken": 86400,
27
+ "sessionToken": 1800,
28
+ "idToken": 3600,
29
+ "authToken": 3600
30
+ },
31
+ "jwtProperties": [
32
+ "exp",
33
+ "expiresIn",
34
+ "expiry",
35
+ "expires",
36
+ "ttl",
37
+ "maxAge",
38
+ "lifetime"
39
+ ],
40
+ "tokenMethods": [
41
+ "sign",
42
+ "create",
43
+ "generate",
44
+ "issue",
45
+ "encode"
46
+ ],
47
+ "jwtLibraries": [
48
+ "jsonwebtoken",
49
+ "jose",
50
+ "@nestjs/jwt",
51
+ "jwt-simple",
52
+ "node-jsonwebtoken",
53
+ "passport-jwt"
54
+ ],
55
+ "sessionMethods": [
56
+ "session",
57
+ "cookie",
58
+ "maxAge",
59
+ "expires"
60
+ ],
61
+ "sessionLibraries": [
62
+ "express-session",
63
+ "connect-session",
64
+ "cookie-session",
65
+ "client-sessions"
66
+ ],
67
+ "oauthProperties": [
68
+ "access_token_lifetime",
69
+ "refresh_token_lifetime",
70
+ "token_lifetime",
71
+ "expires_in"
72
+ ],
73
+ "timeUnits": {
74
+ "seconds": 1,
75
+ "minutes": 60,
76
+ "hours": 3600,
77
+ "days": 86400,
78
+ "weeks": 604800,
79
+ "months": 2592000,
80
+ "years": 31536000
81
+ },
82
+ "dangerousPatterns": [
83
+ "no-expire",
84
+ "never-expire",
85
+ "permanent",
86
+ "forever",
87
+ "infinite",
88
+ "unlimited"
89
+ ],
90
+ "exemptedScenarios": [
91
+ "test",
92
+ "testing",
93
+ "mock",
94
+ "development",
95
+ "dev"
96
+ ]
97
+ },
98
+ "examples": {
99
+ "violations": [
100
+ {
101
+ "description": "JWT token with excessive expiration time",
102
+ "code": "jwt.sign(payload, secret, { expiresIn: '30d' })"
103
+ },
104
+ {
105
+ "description": "Session with very long max age",
106
+ "code": "session({ maxAge: 30 * 24 * 60 * 60 * 1000 })"
107
+ },
108
+ {
109
+ "description": "Token with no expiration",
110
+ "code": "jwt.sign(payload, secret)"
111
+ }
112
+ ],
113
+ "clean": [
114
+ {
115
+ "description": "JWT token with appropriate expiration",
116
+ "code": "jwt.sign(payload, secret, { expiresIn: '1h' })"
117
+ },
118
+ {
119
+ "description": "Session with reasonable max age",
120
+ "code": "session({ maxAge: 30 * 60 * 1000 })"
121
+ }
122
+ ]
123
+ }
124
+ }
@@ -0,0 +1,295 @@
1
+ /**
2
+ * S049 Regex-based Analyzer - Authentication tokens should have short validity periods
3
+ * Fallback analyzer using regex patterns when AST parsing is not available
4
+ */
5
+
6
+ const fs = require("fs");
7
+ const path = require("path");
8
+
9
+ class S049RegexBasedAnalyzer {
10
+ constructor() {
11
+ this.ruleId = "S049";
12
+
13
+ // Load configuration
14
+ const configPath = path.join(__dirname, 'config.json');
15
+ this.config = JSON.parse(fs.readFileSync(configPath, 'utf8')).configuration;
16
+
17
+ this.initializePatterns();
18
+ }
19
+
20
+ async initialize(semanticEngine) {
21
+ // Regex analyzer doesn't need semantic engine
22
+ }
23
+
24
+ /**
25
+ * Initialize regex patterns for detection
26
+ */
27
+ initializePatterns() {
28
+ // JWT token creation patterns
29
+ this.jwtPatterns = [
30
+ // jwt.sign(payload, secret, { expiresIn: 'long-time' }) - handles multiline
31
+ /jwt\.sign\s*\([^)]*,\s*[^)]*,\s*\{[^}]*?expiresIn\s*:\s*['"]\s*([^'"]+)\s*['"][^}]*?\}/gs,
32
+
33
+ // jwt.sign(payload, secret, { exp: timestamp })
34
+ /jwt\.sign\s*\([^)]*,\s*[^)]*,\s*\{[^}]*?exp\s*:\s*(\d+)[^}]*?\}/gs,
35
+
36
+ // Token creation without expiration - matches jwt.sign with exactly 2 parameters
37
+ /jwt\.sign\s*\(\s*[^,)]+\s*,\s*[^,)]+\s*\)/g,
38
+
39
+ // Other JWT libraries
40
+ /(?:jsonwebtoken|jose|@nestjs\/jwt).*\.(?:sign|create|generate)\s*\([^)]*?\)/g
41
+ ];
42
+
43
+ // Session configuration patterns
44
+ this.sessionPatterns = [
45
+ // session({ maxAge: long-time }) - handles multiline
46
+ /session\s*\(\s*\{[^}]*?maxAge\s*:\s*(\d+)[^}]*?\}/gs,
47
+
48
+ // cookie({ maxAge: long-time })
49
+ /cookie\s*\(\s*\{[^}]*?maxAge\s*:\s*(\d+)[^}]*?\}/gs,
50
+
51
+ // express-session configuration
52
+ /require\s*\(\s*['"]\s*express-session\s*['"]\s*\)\s*\(\s*\{[^}]*?maxAge\s*:\s*(\d+)[^}]*?\}/gs
53
+ ];
54
+
55
+ // OAuth token patterns
56
+ this.oauthPatterns = [
57
+ // OAuth configuration with lifetime
58
+ /(?:access_token_lifetime|refresh_token_lifetime|token_lifetime|expires_in)\s*[:=]\s*(\d+)/g
59
+ ];
60
+
61
+ // Dangerous patterns (never expire, etc.)
62
+ this.dangerousPatterns = this.config.dangerousPatterns.map(pattern =>
63
+ new RegExp(pattern, 'gi')
64
+ );
65
+ }
66
+
67
+ /**
68
+ * Analyze file for authentication token validity issues
69
+ */
70
+ async analyze(filePath, language = "typescript", options = {}) {
71
+ try {
72
+ const sourceCode = fs.readFileSync(filePath, "utf8");
73
+ const lines = sourceCode.split('\n');
74
+ const violations = [];
75
+
76
+ // Check for JWT token issues
77
+ this.checkJWTPatterns(sourceCode, lines, filePath, violations);
78
+
79
+ // Check for session configuration issues
80
+ this.checkSessionPatterns(sourceCode, lines, filePath, violations);
81
+
82
+ // Check for OAuth token issues
83
+ this.checkOAuthPatterns(sourceCode, lines, filePath, violations);
84
+
85
+ // Check for dangerous patterns
86
+ this.checkDangerousPatterns(sourceCode, lines, filePath, violations);
87
+
88
+ return violations;
89
+ } catch (error) {
90
+ console.error(`🔧 [S049] Error in regex-based analysis:`, error);
91
+ return [];
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Check JWT patterns for violations
97
+ */
98
+ checkJWTPatterns(sourceCode, lines, filePath, violations) {
99
+ for (const pattern of this.jwtPatterns) {
100
+ let match;
101
+ const globalPattern = new RegExp(pattern.source, 'g');
102
+
103
+ while ((match = globalPattern.exec(sourceCode)) !== null) {
104
+ const lineInfo = this.getLineInfo(sourceCode, match.index);
105
+
106
+ if (this.isInExemptedContext(lines[lineInfo.line - 1])) {
107
+ continue;
108
+ }
109
+
110
+ // Check if this is a token creation without expiration
111
+ if (!match[1]) {
112
+ this.addViolation(violations, filePath, lineInfo, lines[lineInfo.line - 1],
113
+ 'JWT token created without expiration time',
114
+ 'missing-expiration');
115
+ continue;
116
+ }
117
+
118
+ // Check if expiration time is too long
119
+ const expirationValue = match[1];
120
+ const seconds = this.parseTimeValue(expirationValue);
121
+
122
+ if (seconds > this.config.maxValidityPeriods.accessToken) {
123
+ this.addViolation(violations, filePath, lineInfo, lines[lineInfo.line - 1],
124
+ `JWT token expiration time (${expirationValue}) exceeds recommended maximum of ${this.config.maxValidityPeriods.accessToken} seconds`,
125
+ 'long-expiration');
126
+ }
127
+ }
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Check session patterns for violations
133
+ */
134
+ checkSessionPatterns(sourceCode, lines, filePath, violations) {
135
+ for (const pattern of this.sessionPatterns) {
136
+ let match;
137
+ const globalPattern = new RegExp(pattern.source, 'g');
138
+
139
+ while ((match = globalPattern.exec(sourceCode)) !== null) {
140
+ const lineInfo = this.getLineInfo(sourceCode, match.index);
141
+
142
+ if (this.isInExemptedContext(lines[lineInfo.line - 1])) {
143
+ continue;
144
+ }
145
+
146
+ const maxAgeValue = match[1];
147
+ if (maxAgeValue) {
148
+ // Session values are often in milliseconds
149
+ const seconds = Math.floor(parseInt(maxAgeValue) / 1000);
150
+
151
+ if (seconds > this.config.maxValidityPeriods.sessionToken) {
152
+ this.addViolation(violations, filePath, lineInfo, lines[lineInfo.line - 1],
153
+ `Session maxAge (${maxAgeValue}ms) exceeds recommended maximum of ${this.config.maxValidityPeriods.sessionToken} seconds`,
154
+ 'long-session');
155
+ }
156
+ }
157
+ }
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Check OAuth patterns for violations
163
+ */
164
+ checkOAuthPatterns(sourceCode, lines, filePath, violations) {
165
+ for (const pattern of this.oauthPatterns) {
166
+ let match;
167
+ const globalPattern = new RegExp(pattern.source, 'g');
168
+
169
+ while ((match = globalPattern.exec(sourceCode)) !== null) {
170
+ const lineInfo = this.getLineInfo(sourceCode, match.index);
171
+
172
+ if (this.isInExemptedContext(lines[lineInfo.line - 1])) {
173
+ continue;
174
+ }
175
+
176
+ const tokenValue = match[1];
177
+ if (tokenValue) {
178
+ const seconds = parseInt(tokenValue);
179
+
180
+ if (seconds > this.config.maxValidityPeriods.accessToken) {
181
+ this.addViolation(violations, filePath, lineInfo, lines[lineInfo.line - 1],
182
+ `OAuth token lifetime (${tokenValue}s) exceeds recommended maximum of ${this.config.maxValidityPeriods.accessToken} seconds`,
183
+ 'long-oauth-token');
184
+ }
185
+ }
186
+ }
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Check for dangerous patterns (never expire, etc.)
192
+ */
193
+ checkDangerousPatterns(sourceCode, lines, filePath, violations) {
194
+ for (const pattern of this.dangerousPatterns) {
195
+ let match;
196
+
197
+ while ((match = pattern.exec(sourceCode)) !== null) {
198
+ const lineInfo = this.getLineInfo(sourceCode, match.index);
199
+
200
+ if (this.isInExemptedContext(lines[lineInfo.line - 1])) {
201
+ continue;
202
+ }
203
+
204
+ this.addViolation(violations, filePath, lineInfo, lines[lineInfo.line - 1],
205
+ `Dangerous token configuration detected: "${match[0]}" suggests tokens that never expire`,
206
+ 'dangerous-pattern');
207
+ }
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Check if line is in exempted context (test, mock, etc.)
213
+ */
214
+ isInExemptedContext(line) {
215
+ if (!line) return false;
216
+
217
+ const lowerLine = line.toLowerCase();
218
+ return this.config.exemptedScenarios.some(scenario =>
219
+ lowerLine.includes(scenario)
220
+ );
221
+ }
222
+
223
+ /**
224
+ * Parse time value to seconds
225
+ */
226
+ parseTimeValue(value) {
227
+ if (typeof value === 'number') {
228
+ return value;
229
+ }
230
+
231
+ if (typeof value === 'string') {
232
+ // Handle string formats like '1h', '30d', '2w'
233
+ const match = value.match(/^(\d+)\s*([a-zA-Z]+)?$/);
234
+ if (match) {
235
+ const num = parseInt(match[1]);
236
+ const unit = match[2]?.toLowerCase() || 's';
237
+
238
+ // Map common units
239
+ const multiplier = this.config.timeUnits[unit] ||
240
+ this.config.timeUnits[unit + 's'] || 1;
241
+
242
+ return num * multiplier;
243
+ }
244
+
245
+ // Handle pure numbers as strings
246
+ const numValue = parseInt(value);
247
+ if (!isNaN(numValue)) {
248
+ return numValue;
249
+ }
250
+ }
251
+
252
+ return 0;
253
+ }
254
+
255
+ /**
256
+ * Get line information from source position
257
+ */
258
+ getLineInfo(sourceCode, index) {
259
+ const beforeMatch = sourceCode.substring(0, index);
260
+ const line = beforeMatch.split('\n').length;
261
+ const lineStart = beforeMatch.lastIndexOf('\n') + 1;
262
+ const column = index - lineStart;
263
+
264
+ return { line, column };
265
+ }
266
+
267
+ /**
268
+ * Add violation to results
269
+ */
270
+ addViolation(violations, filePath, lineInfo, sourceLine, message, subType) {
271
+ violations.push({
272
+ ruleId: this.ruleId,
273
+ message,
274
+ severity: "error",
275
+ line: lineInfo.line,
276
+ column: lineInfo.column,
277
+ endLine: lineInfo.line,
278
+ endColumn: lineInfo.column + (sourceLine?.length || 0),
279
+ source: sourceLine || "",
280
+ filePath,
281
+ type: "regex-based",
282
+ subType,
283
+ context: {
284
+ matchType: "pattern-based",
285
+ pattern: subType
286
+ }
287
+ });
288
+ }
289
+
290
+ cleanup() {
291
+ // Cleanup resources if needed
292
+ }
293
+ }
294
+
295
+ module.exports = S049RegexBasedAnalyzer;