@sun-asterisk/sunlint 1.3.29 → 1.3.31
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/origin-rules/common-en.md +14 -0
- package/package.json +1 -1
- package/rules/common/C006_function_naming/analyzer.js +279 -143
- package/rules/common/C067_no_hardcoded_config/symbol-based-analyzer.js +2 -1
- package/rules/security/S031_secure_session_cookies/analyzer.js +67 -112
- package/rules/security/S041_session_token_invalidation/analyzer.js +1 -1
- package/rules/security/S041_session_token_invalidation/symbol-based-analyzer.js +21 -2
- package/rules/security/S042_require_re_authentication_for_long_lived/symbol-based-analyzer.js +97 -4
- package/rules/security/S031_secure_session_cookies/regex-based-analyzer.js +0 -296
|
@@ -1,296 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* S031 Regex-Based Analyzer - Set Secure flag for Session Cookies
|
|
3
|
-
* Fallback analysis using regex patterns
|
|
4
|
-
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
const fs = require("fs");
|
|
8
|
-
|
|
9
|
-
class S031RegexBasedAnalyzer {
|
|
10
|
-
constructor(semanticEngine = null) {
|
|
11
|
-
this.semanticEngine = semanticEngine;
|
|
12
|
-
this.ruleId = "S031";
|
|
13
|
-
this.category = "security";
|
|
14
|
-
|
|
15
|
-
// Session cookie indicators
|
|
16
|
-
this.sessionIndicators = [
|
|
17
|
-
"session",
|
|
18
|
-
"sessionid",
|
|
19
|
-
"sessid",
|
|
20
|
-
"jsessionid",
|
|
21
|
-
"phpsessid",
|
|
22
|
-
"asp.net_sessionid",
|
|
23
|
-
"connect.sid",
|
|
24
|
-
"auth",
|
|
25
|
-
"token",
|
|
26
|
-
"jwt",
|
|
27
|
-
"csrf",
|
|
28
|
-
];
|
|
29
|
-
|
|
30
|
-
// Regex patterns for cookie detection
|
|
31
|
-
this.cookiePatterns = [
|
|
32
|
-
// Express/Node.js patterns
|
|
33
|
-
/res\.cookie\s*\(\s*['"`]([^'"`]+)['"`]\s*,([^)]+)\)/gi,
|
|
34
|
-
/response\.cookie\s*\(\s*['"`]([^'"`]+)['"`]\s*,([^)]+)\)/gi,
|
|
35
|
-
/\.setCookie\s*\(\s*['"`]([^'"`]+)['"`]\s*,([^)]+)\)/gi,
|
|
36
|
-
|
|
37
|
-
// Set-Cookie header patterns
|
|
38
|
-
/setHeader\s*\(\s*['"`]Set-Cookie['"`]\s*,\s*['"`]([^'"`]+)['"`]\s*\)/gi,
|
|
39
|
-
/writeHead\s*\([^,]*,\s*{[^}]*['"`]Set-Cookie['"`]\s*:\s*['"`]([^'"`]+)['"`]/gi,
|
|
40
|
-
|
|
41
|
-
// Document.cookie assignments
|
|
42
|
-
/document\.cookie\s*=\s*['"`]([^'"`]+)['"`]/gi,
|
|
43
|
-
|
|
44
|
-
// Session middleware patterns
|
|
45
|
-
/session\s*\(\s*{([^}]+)}/gi,
|
|
46
|
-
/\.use\s*\(\s*session\s*\(\s*{([^}]+)}/gi,
|
|
47
|
-
];
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Initialize analyzer
|
|
52
|
-
|
|
53
|
-
*/
|
|
54
|
-
async initialize(semanticEngine) {
|
|
55
|
-
this.semanticEngine = semanticEngine;
|
|
56
|
-
if (process.env.SUNLINT_DEBUG) {
|
|
57
|
-
console.log(`🔧 [S031] Regex-based analyzer initialized`);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Check if file should be skipped (test files)
|
|
63
|
-
*/
|
|
64
|
-
shouldSkipFile(filePath) {
|
|
65
|
-
const testPatterns = [
|
|
66
|
-
/\.test\.(ts|tsx|js|jsx)$/,
|
|
67
|
-
/\.spec\.(ts|tsx|js|jsx)$/,
|
|
68
|
-
/__tests__\//,
|
|
69
|
-
/__mocks__\//,
|
|
70
|
-
/\/tests?\//,
|
|
71
|
-
/\/fixtures?\//,
|
|
72
|
-
];
|
|
73
|
-
return testPatterns.some((pattern) => pattern.test(filePath));
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Analyze file content using regex patterns
|
|
78
|
-
|
|
79
|
-
*/
|
|
80
|
-
async analyze(filePath) {
|
|
81
|
-
if (process.env.SUNLINT_DEBUG) {
|
|
82
|
-
console.log(`🔍 [S031] Regex-based analysis for: ${filePath}`);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Skip test files
|
|
86
|
-
if (this.shouldSkipFile(filePath)) {
|
|
87
|
-
if (process.env.SUNLINT_DEBUG) {
|
|
88
|
-
console.log(`⏭ [S031] Skipping test file: ${filePath}`);
|
|
89
|
-
}
|
|
90
|
-
return [];
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
let content;
|
|
94
|
-
try {
|
|
95
|
-
content = fs.readFileSync(filePath, "utf8");
|
|
96
|
-
} catch (error) {
|
|
97
|
-
if (process.env.SUNLINT_DEBUG) {
|
|
98
|
-
console.error(`❌ [S031] File read error:`, error);
|
|
99
|
-
}
|
|
100
|
-
throw error;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const violations = [];
|
|
104
|
-
const lines = content.split("\n");
|
|
105
|
-
|
|
106
|
-
// Check each pattern
|
|
107
|
-
for (const pattern of this.cookiePatterns) {
|
|
108
|
-
this.checkPattern(pattern, content, lines, violations, filePath);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Check for custom cookie utilities (e.g., StorageUtils.setCookie)
|
|
112
|
-
this.checkCustomCookieUtilities(content, lines, violations, filePath);
|
|
113
|
-
|
|
114
|
-
return violations;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Check specific regex pattern for violations
|
|
119
|
-
|
|
120
|
-
*/
|
|
121
|
-
checkPattern(pattern, content, lines, violations, filePath) {
|
|
122
|
-
let match;
|
|
123
|
-
pattern.lastIndex = 0; // Reset regex state
|
|
124
|
-
|
|
125
|
-
while ((match = pattern.exec(content)) !== null) {
|
|
126
|
-
const matchText = match[0];
|
|
127
|
-
const cookieName = match[1] || "";
|
|
128
|
-
const cookieOptions = match[2] || match[1] || "";
|
|
129
|
-
|
|
130
|
-
// Check if this is a session cookie
|
|
131
|
-
if (!this.isSessionCookie(cookieName, matchText)) {
|
|
132
|
-
continue;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Check if secure flag is present
|
|
136
|
-
if (!this.hasSecureFlag(cookieOptions, matchText)) {
|
|
137
|
-
const lineNumber = this.getLineNumber(content, match.index);
|
|
138
|
-
|
|
139
|
-
this.addViolation(
|
|
140
|
-
matchText,
|
|
141
|
-
lineNumber,
|
|
142
|
-
violations,
|
|
143
|
-
`Session cookie "${cookieName || "unknown"}" missing Secure flag`
|
|
144
|
-
);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Check if cookie name or context indicates session cookie
|
|
151
|
-
|
|
152
|
-
*/
|
|
153
|
-
isSessionCookie(cookieName, matchText) {
|
|
154
|
-
const textToCheck = (cookieName + " " + matchText).toLowerCase();
|
|
155
|
-
return this.sessionIndicators.some((indicator) =>
|
|
156
|
-
textToCheck.includes(indicator.toLowerCase())
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Check if secure flag is present in cookie options
|
|
162
|
-
*/
|
|
163
|
-
hasSecureFlag(cookieOptions, fullMatch) {
|
|
164
|
-
const textToCheck = cookieOptions + " " + fullMatch;
|
|
165
|
-
|
|
166
|
-
// Check for secure config references (likely safe)
|
|
167
|
-
const secureConfigPatterns = [
|
|
168
|
-
/\bcookieConfig\b/i,
|
|
169
|
-
/\bsecureConfig\b/i,
|
|
170
|
-
/\bsafeConfig\b/i,
|
|
171
|
-
/\bdefaultConfig\b/i,
|
|
172
|
-
/\.\.\..*config/i, // spread operator with config
|
|
173
|
-
/config.*secure/i,
|
|
174
|
-
];
|
|
175
|
-
|
|
176
|
-
// If using a secure config reference, assume it's safe
|
|
177
|
-
if (secureConfigPatterns.some((pattern) => pattern.test(textToCheck))) {
|
|
178
|
-
return true;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Check for various secure flag patterns
|
|
182
|
-
const securePatterns = [
|
|
183
|
-
/secure\s*:\s*true/i,
|
|
184
|
-
/secure\s*=\s*true/i,
|
|
185
|
-
/;\s*secure\s*[;\s]/i,
|
|
186
|
-
/;\s*secure$/i,
|
|
187
|
-
/['"`]\s*secure\s*['"`]/i,
|
|
188
|
-
/"secure"\s*:\s*true/i,
|
|
189
|
-
/'secure'\s*:\s*true/i,
|
|
190
|
-
/\bsecure\b/i, // Simple secure keyword
|
|
191
|
-
];
|
|
192
|
-
|
|
193
|
-
return securePatterns.some((pattern) => pattern.test(textToCheck));
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* Get line number from content position
|
|
198
|
-
|
|
199
|
-
*/
|
|
200
|
-
getLineNumber(content, position) {
|
|
201
|
-
const beforeMatch = content.substring(0, position);
|
|
202
|
-
return beforeMatch.split("\n").length;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Add violation to results
|
|
207
|
-
|
|
208
|
-
*/
|
|
209
|
-
addViolation(source, lineNumber, violations, message) {
|
|
210
|
-
violations.push({
|
|
211
|
-
ruleId: this.ruleId,
|
|
212
|
-
source: source.trim(),
|
|
213
|
-
category: this.category,
|
|
214
|
-
line: lineNumber,
|
|
215
|
-
column: 1,
|
|
216
|
-
message: `Insecure session cookie: ${message}`,
|
|
217
|
-
severity: "error",
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Check for custom cookie utility functions with variable names
|
|
223
|
-
* Handles cases like: StorageUtils.setCookie(STORAGE_KEY.ACCESS_TOKEN, value)
|
|
224
|
-
*/
|
|
225
|
-
checkCustomCookieUtilities(content, lines, violations, filePath) {
|
|
226
|
-
// Pattern to match custom cookie utilities with variable references
|
|
227
|
-
// Matches: Utils.setCookie(VARIABLE_NAME, value) or Utils.setCookie(VARIABLE_NAME, value, options)
|
|
228
|
-
const customCookiePattern = /(\w+\.setCookie)\s*\(\s*([A-Z_][A-Z0-9_.]*)\s*,\s*([^,)]+)(?:\s*,\s*([^)]*))?\s*\)/gi;
|
|
229
|
-
|
|
230
|
-
let match;
|
|
231
|
-
customCookiePattern.lastIndex = 0;
|
|
232
|
-
|
|
233
|
-
while ((match = customCookiePattern.exec(content)) !== null) {
|
|
234
|
-
const methodCall = match[1]; // e.g., "StorageUtils.setCookie"
|
|
235
|
-
const cookieNameVar = match[2]; // e.g., "STORAGE_KEY.ACCESS_TOKEN"
|
|
236
|
-
const cookieValue = match[3]; // e.g., "response.user?.access_token || ''"
|
|
237
|
-
const cookieOptions = match[4] || ""; // e.g., options object if present
|
|
238
|
-
const matchText = match[0];
|
|
239
|
-
|
|
240
|
-
// Check if this looks like a session cookie based on variable name or value
|
|
241
|
-
if (!this.isSessionCookieLikely(cookieNameVar, cookieValue, matchText)) {
|
|
242
|
-
continue;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// Check if secure flag is present in options
|
|
246
|
-
if (!this.hasSecureFlag(cookieOptions, matchText)) {
|
|
247
|
-
const lineNumber = this.getLineNumber(content, match.index);
|
|
248
|
-
|
|
249
|
-
// Extract a friendly cookie name from the variable
|
|
250
|
-
const friendlyCookieName = this.extractFriendlyCookieName(cookieNameVar);
|
|
251
|
-
|
|
252
|
-
this.addViolation(
|
|
253
|
-
matchText,
|
|
254
|
-
lineNumber,
|
|
255
|
-
violations,
|
|
256
|
-
`Session cookie from "${friendlyCookieName}" missing Secure flag - add secure flag to options`
|
|
257
|
-
);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* Check if cookie variable name or value suggests it's a session cookie
|
|
264
|
-
*/
|
|
265
|
-
isSessionCookieLikely(varName, value, matchText) {
|
|
266
|
-
const textToCheck = (varName + " " + value + " " + matchText).toLowerCase();
|
|
267
|
-
|
|
268
|
-
// Check against session indicators
|
|
269
|
-
const isSession = this.sessionIndicators.some((indicator) =>
|
|
270
|
-
textToCheck.includes(indicator.toLowerCase())
|
|
271
|
-
);
|
|
272
|
-
|
|
273
|
-
// Also check for common patterns like ACCESS_TOKEN, REFRESH_TOKEN, etc.
|
|
274
|
-
const tokenPatterns = [
|
|
275
|
-
/access[_-]?token/i,
|
|
276
|
-
/refresh[_-]?token/i,
|
|
277
|
-
/auth[_-]?token/i,
|
|
278
|
-
/id[_-]?token/i,
|
|
279
|
-
/session/i,
|
|
280
|
-
];
|
|
281
|
-
|
|
282
|
-
return isSession || tokenPatterns.some(pattern => pattern.test(textToCheck));
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
/**
|
|
286
|
-
* Extract a friendly cookie name from variable reference
|
|
287
|
-
* e.g., "STORAGE_KEY.ACCESS_TOKEN" -> "ACCESS_TOKEN"
|
|
288
|
-
*/
|
|
289
|
-
extractFriendlyCookieName(varName) {
|
|
290
|
-
// If it has a dot, take the last part
|
|
291
|
-
const parts = varName.split(".");
|
|
292
|
-
return parts[parts.length - 1] || varName;
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
module.exports = S031RegexBasedAnalyzer;
|