@sun-asterisk/sunlint 1.3.2 → 1.3.4
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 +73 -0
- package/README.md +5 -3
- package/config/rules/enhanced-rules-registry.json +144 -33
- package/core/analysis-orchestrator.js +173 -42
- package/core/auto-performance-manager.js +243 -0
- package/core/cli-action-handler.js +24 -2
- package/core/cli-program.js +19 -5
- package/core/constants/defaults.js +56 -0
- package/core/performance-optimizer.js +271 -0
- package/docs/FILE_LIMITS_COMPLETION_REPORT.md +151 -0
- package/docs/FILE_LIMITS_EXPLANATION.md +190 -0
- package/docs/PERFORMANCE.md +311 -0
- package/docs/PERFORMANCE_MIGRATION_GUIDE.md +368 -0
- package/docs/PERFORMANCE_OPTIMIZATION_PLAN.md +255 -0
- package/docs/QUICK_FILE_LIMITS.md +64 -0
- package/docs/SIMPLIFIED_USAGE_GUIDE.md +208 -0
- package/engines/engine-factory.js +7 -0
- package/engines/heuristic-engine.js +182 -5
- package/package.json +2 -1
- package/rules/common/C048_no_bypass_architectural_layers/analyzer.js +180 -0
- package/rules/common/C048_no_bypass_architectural_layers/config.json +50 -0
- package/rules/common/C048_no_bypass_architectural_layers/symbol-based-analyzer.js +235 -0
- package/rules/common/C052_parsing_or_data_transformation/analyzer.js +180 -0
- package/rules/common/C052_parsing_or_data_transformation/config.json +50 -0
- package/rules/common/C052_parsing_or_data_transformation/symbol-based-analyzer.js +132 -0
- package/rules/index.js +2 -0
- package/rules/security/S017_use_parameterized_queries/README.md +128 -0
- package/rules/security/S017_use_parameterized_queries/analyzer.js +286 -0
- package/rules/security/S017_use_parameterized_queries/config.json +109 -0
- package/rules/security/S017_use_parameterized_queries/regex-based-analyzer.js +541 -0
- package/rules/security/S017_use_parameterized_queries/symbol-based-analyzer.js +777 -0
- package/rules/security/S031_secure_session_cookies/README.md +127 -0
- package/rules/security/S031_secure_session_cookies/analyzer.js +245 -0
- package/rules/security/S031_secure_session_cookies/config.json +86 -0
- package/rules/security/S031_secure_session_cookies/regex-based-analyzer.js +196 -0
- package/rules/security/S031_secure_session_cookies/symbol-based-analyzer.js +1084 -0
- package/rules/security/S032_httponly_session_cookies/FRAMEWORK_SUPPORT.md +209 -0
- package/rules/security/S032_httponly_session_cookies/README.md +184 -0
- package/rules/security/S032_httponly_session_cookies/analyzer.js +282 -0
- package/rules/security/S032_httponly_session_cookies/config.json +96 -0
- package/rules/security/S032_httponly_session_cookies/regex-based-analyzer.js +715 -0
- package/rules/security/S032_httponly_session_cookies/symbol-based-analyzer.js +1348 -0
- package/rules/security/S033_samesite_session_cookies/README.md +227 -0
- package/rules/security/S033_samesite_session_cookies/analyzer.js +242 -0
- package/rules/security/S033_samesite_session_cookies/config.json +87 -0
- package/rules/security/S033_samesite_session_cookies/regex-based-analyzer.js +703 -0
- package/rules/security/S033_samesite_session_cookies/symbol-based-analyzer.js +732 -0
- package/rules/security/S034_host_prefix_session_cookies/README.md +204 -0
- package/rules/security/S034_host_prefix_session_cookies/analyzer.js +290 -0
- package/rules/security/S034_host_prefix_session_cookies/config.json +62 -0
- package/rules/security/S034_host_prefix_session_cookies/regex-based-analyzer.js +478 -0
- package/rules/security/S034_host_prefix_session_cookies/symbol-based-analyzer.js +277 -0
- package/rules/security/S035_path_session_cookies/README.md +257 -0
- package/rules/security/S035_path_session_cookies/analyzer.js +316 -0
- package/rules/security/S035_path_session_cookies/config.json +99 -0
- package/rules/security/S035_path_session_cookies/regex-based-analyzer.js +724 -0
- package/rules/security/S035_path_session_cookies/symbol-based-analyzer.js +373 -0
- package/scripts/batch-processing-demo.js +334 -0
- package/scripts/performance-test.js +541 -0
- package/scripts/quick-performance-test.js +108 -0
|
@@ -0,0 +1,1084 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S031 Symbol-Based Analyzer - Set Secure flag for Session Cookies
|
|
3
|
+
* Uses TypeScript compiler API for semantic analysis
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const ts = require("typescript");
|
|
7
|
+
|
|
8
|
+
class S031SymbolBasedAnalyzer {
|
|
9
|
+
constructor(semanticEngine = null) {
|
|
10
|
+
this.semanticEngine = semanticEngine;
|
|
11
|
+
this.ruleId = "S031";
|
|
12
|
+
this.category = "security";
|
|
13
|
+
|
|
14
|
+
// Session cookie indicators
|
|
15
|
+
this.sessionIndicators = [
|
|
16
|
+
"session",
|
|
17
|
+
"sessionid",
|
|
18
|
+
"sessid",
|
|
19
|
+
"jsessionid",
|
|
20
|
+
"phpsessid",
|
|
21
|
+
"asp.net_sessionid",
|
|
22
|
+
"connect.sid",
|
|
23
|
+
"auth",
|
|
24
|
+
"token",
|
|
25
|
+
"jwt",
|
|
26
|
+
"csrf",
|
|
27
|
+
"refresh",
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
// Cookie methods that need security checking
|
|
31
|
+
this.cookieMethods = [
|
|
32
|
+
"setCookie",
|
|
33
|
+
"cookie",
|
|
34
|
+
"set",
|
|
35
|
+
"append",
|
|
36
|
+
"session",
|
|
37
|
+
"setHeader",
|
|
38
|
+
"writeHead",
|
|
39
|
+
];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Initialize analyzer with semantic engine
|
|
44
|
+
*/
|
|
45
|
+
async initialize(semanticEngine) {
|
|
46
|
+
this.semanticEngine = semanticEngine;
|
|
47
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
48
|
+
console.log(`🔧 [S031] Symbol-based analyzer initialized`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Analyze source file for insecure session cookies
|
|
54
|
+
*/
|
|
55
|
+
async analyze(sourceFile, filePath) {
|
|
56
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
57
|
+
console.log(`🔍 [S031] Symbol-based analysis for: ${filePath}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const violations = [];
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
// Get the TypeScript compiler SourceFile from ts-morph
|
|
64
|
+
const compilerNode = sourceFile.compilerNode || sourceFile._compilerNode;
|
|
65
|
+
if (!compilerNode) {
|
|
66
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
67
|
+
console.log(`⚠️ [S031] No compiler node found, using ts-morph API`);
|
|
68
|
+
}
|
|
69
|
+
// Use ts-morph API instead
|
|
70
|
+
this.visitMorphNode(sourceFile, violations, sourceFile);
|
|
71
|
+
} else {
|
|
72
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
73
|
+
console.log(`✅ [S031] Using TypeScript compiler API`);
|
|
74
|
+
}
|
|
75
|
+
// Traverse AST to find cookie-related code
|
|
76
|
+
this.visitNode(compilerNode, violations, compilerNode);
|
|
77
|
+
}
|
|
78
|
+
} catch (error) {
|
|
79
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
80
|
+
console.error(`❌ [S031] Symbol analysis error:`, error);
|
|
81
|
+
}
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return violations;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Visit ts-morph nodes recursively
|
|
90
|
+
*/
|
|
91
|
+
visitMorphNode(node, violations, sourceFile) {
|
|
92
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
93
|
+
const nodeKind = node.getKindName ? node.getKindName() : "Unknown";
|
|
94
|
+
if (
|
|
95
|
+
nodeKind &&
|
|
96
|
+
(nodeKind.includes("Call") || nodeKind.includes("Property"))
|
|
97
|
+
) {
|
|
98
|
+
console.log(`🔍 [S031] Symbol: Visiting ${nodeKind} node (ts-morph)`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Check for call expressions
|
|
103
|
+
if (node.getKind && node.getKind() === 208) {
|
|
104
|
+
// CallExpression
|
|
105
|
+
this.checkMorphCookieMethodCall(node, violations, sourceFile);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Continue traversing children
|
|
109
|
+
if (node.getChildren) {
|
|
110
|
+
node.getChildren().forEach((child) => {
|
|
111
|
+
this.visitMorphNode(child, violations, sourceFile);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Check cookie method calls using ts-morph API
|
|
118
|
+
*/
|
|
119
|
+
checkMorphCookieMethodCall(callNode, violations, sourceFile) {
|
|
120
|
+
const methodName = this.getMorphMethodName(callNode);
|
|
121
|
+
|
|
122
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
123
|
+
console.log(
|
|
124
|
+
`🔍 [S031] Symbol: ts-morph Method call detected: "${methodName}"`
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!this.cookieMethods.includes(methodName)) {
|
|
129
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
130
|
+
console.log(
|
|
131
|
+
`🔍 [S031] Symbol: Method "${methodName}" not in cookieMethods list`
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
138
|
+
console.log(
|
|
139
|
+
`🔍 [S031] Symbol: Method "${methodName}" found in cookieMethods, proceeding...`
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Special handling for setHeader("Set-Cookie", [...]) pattern
|
|
144
|
+
if (methodName === "setHeader") {
|
|
145
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
146
|
+
console.log(
|
|
147
|
+
`🔍 [S031] Symbol: Special setHeader handling triggered for line ${
|
|
148
|
+
callNode.getStartLineNumber?.() || "unknown"
|
|
149
|
+
}`
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
this.checkSetHeaderCookies(callNode, violations, sourceFile);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Check if this is setting a session-related cookie
|
|
157
|
+
const cookieName = this.extractMorphCookieName(callNode);
|
|
158
|
+
if (!this.isSessionCookie(cookieName, callNode)) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Check for secure flag in options
|
|
163
|
+
const hasSecureFlag = this.checkMorphSecureFlag(callNode);
|
|
164
|
+
|
|
165
|
+
if (!hasSecureFlag) {
|
|
166
|
+
this.addMorphViolation(
|
|
167
|
+
callNode,
|
|
168
|
+
violations,
|
|
169
|
+
sourceFile,
|
|
170
|
+
`Session cookie "${cookieName || "unknown"}" missing Secure flag`
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Extract method name from ts-morph call expression
|
|
177
|
+
*/
|
|
178
|
+
getMorphMethodName(callNode) {
|
|
179
|
+
try {
|
|
180
|
+
const expression = callNode.getExpression();
|
|
181
|
+
if (expression && expression.getKind() === 201) {
|
|
182
|
+
// PropertyAccessExpression
|
|
183
|
+
return expression.getName();
|
|
184
|
+
}
|
|
185
|
+
if (expression && expression.getText) {
|
|
186
|
+
return expression.getText().split(".").pop();
|
|
187
|
+
}
|
|
188
|
+
} catch (error) {
|
|
189
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
190
|
+
console.log(
|
|
191
|
+
`🔍 [S031] Symbol: Error getting method name:`,
|
|
192
|
+
error.message
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return "";
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Check setHeader("Set-Cookie", [...]) pattern for insecure session cookies
|
|
201
|
+
*/
|
|
202
|
+
checkSetHeaderCookies(callNode, violations, sourceFile) {
|
|
203
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
204
|
+
console.log(
|
|
205
|
+
`🔍 [S031] Symbol: checkSetHeaderCookies called for line ${
|
|
206
|
+
callNode.getStartLineNumber?.() || "unknown"
|
|
207
|
+
}`
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
const args = callNode.getArguments();
|
|
213
|
+
if (!args || args.length < 2) {
|
|
214
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
215
|
+
console.log(
|
|
216
|
+
`🔍 [S031] Symbol: setHeader insufficient args: ${
|
|
217
|
+
args?.length || 0
|
|
218
|
+
}`
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Check if first argument is "Set-Cookie"
|
|
225
|
+
const firstArg = args[0];
|
|
226
|
+
const headerName = firstArg.getText().replace(/['"]/g, "");
|
|
227
|
+
|
|
228
|
+
if (headerName !== "Set-Cookie") {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Get the array of cookie strings from second argument
|
|
233
|
+
const secondArg = args[1];
|
|
234
|
+
if (!secondArg) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Parse cookie strings from array
|
|
239
|
+
const cookieStrings = this.extractCookieStringsFromArray(secondArg);
|
|
240
|
+
|
|
241
|
+
for (const cookieString of cookieStrings) {
|
|
242
|
+
const cookieName = this.extractCookieNameFromString(cookieString);
|
|
243
|
+
|
|
244
|
+
if (this.isSessionCookie(cookieName, null)) {
|
|
245
|
+
const hasSecure = cookieString.toLowerCase().includes("secure");
|
|
246
|
+
|
|
247
|
+
if (!hasSecure) {
|
|
248
|
+
this.addMorphViolation(
|
|
249
|
+
callNode,
|
|
250
|
+
violations,
|
|
251
|
+
sourceFile,
|
|
252
|
+
`Session cookie "${cookieName}" in Set-Cookie header missing Secure attribute`
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
} catch (error) {
|
|
258
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
259
|
+
console.log(
|
|
260
|
+
`🔍 [S031] Symbol: Error checking setHeader cookies:`,
|
|
261
|
+
error.message
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Extract cookie strings from array literal or template strings
|
|
269
|
+
*/
|
|
270
|
+
extractCookieStringsFromArray(arrayNode) {
|
|
271
|
+
const cookieStrings = [];
|
|
272
|
+
|
|
273
|
+
try {
|
|
274
|
+
if (arrayNode.getKind() === 196) {
|
|
275
|
+
// ArrayLiteralExpression
|
|
276
|
+
const elements = arrayNode.getElements();
|
|
277
|
+
|
|
278
|
+
for (const element of elements) {
|
|
279
|
+
let cookieString = element.getText();
|
|
280
|
+
|
|
281
|
+
// Remove quotes and template literal markers
|
|
282
|
+
cookieString = cookieString
|
|
283
|
+
.replace(/^[`'"]/g, "")
|
|
284
|
+
.replace(/[`'"]$/g, "");
|
|
285
|
+
|
|
286
|
+
// Handle template literals with variables
|
|
287
|
+
if (cookieString.includes("${")) {
|
|
288
|
+
// Extract cookie name from template pattern like `auth=${tokens.auth}; ...`
|
|
289
|
+
const match = cookieString.match(/^(\w+)=/);
|
|
290
|
+
if (match) {
|
|
291
|
+
cookieStrings.push(cookieString);
|
|
292
|
+
}
|
|
293
|
+
} else {
|
|
294
|
+
cookieStrings.push(cookieString);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
} catch (error) {
|
|
299
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
300
|
+
console.log(
|
|
301
|
+
`🔍 [S031] Symbol: Error extracting cookie strings:`,
|
|
302
|
+
error.message
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return cookieStrings;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Extract cookie name from cookie string like "auth=value; HttpOnly; ..."
|
|
312
|
+
*/
|
|
313
|
+
extractCookieNameFromString(cookieString) {
|
|
314
|
+
try {
|
|
315
|
+
const match = cookieString.match(/^(\w+)=/);
|
|
316
|
+
return match ? match[1] : null;
|
|
317
|
+
} catch (error) {
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Extract cookie name from ts-morph method call
|
|
324
|
+
*/
|
|
325
|
+
extractMorphCookieName(callNode) {
|
|
326
|
+
try {
|
|
327
|
+
const args = callNode.getArguments();
|
|
328
|
+
if (args && args.length > 0) {
|
|
329
|
+
const firstArg = args[0];
|
|
330
|
+
if (firstArg && firstArg.getText) {
|
|
331
|
+
const text = firstArg.getText();
|
|
332
|
+
return text.replace(/['"]/g, ""); // Remove quotes
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
} catch (error) {
|
|
336
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
337
|
+
console.log(
|
|
338
|
+
`🔍 [S031] Symbol: Error extracting cookie name:`,
|
|
339
|
+
error.message
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Check for secure flag in ts-morph method call options
|
|
348
|
+
*/
|
|
349
|
+
checkMorphSecureFlag(callNode) {
|
|
350
|
+
try {
|
|
351
|
+
const args = callNode.getArguments();
|
|
352
|
+
if (!args || args.length < 2) {
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Check options object (usually second or third argument)
|
|
357
|
+
for (let i = 1; i < args.length; i++) {
|
|
358
|
+
const arg = args[i];
|
|
359
|
+
if (arg && arg.getKind && arg.getKind() === 195) {
|
|
360
|
+
// ObjectLiteralExpression
|
|
361
|
+
const text = arg.getText();
|
|
362
|
+
if (
|
|
363
|
+
text.includes("secure") &&
|
|
364
|
+
(text.includes("true") || text.includes(": true"))
|
|
365
|
+
) {
|
|
366
|
+
return true;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
} catch (error) {
|
|
371
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
372
|
+
console.log(
|
|
373
|
+
`🔍 [S031] Symbol: Error checking secure flag:`,
|
|
374
|
+
error.message
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Add violation using ts-morph API
|
|
383
|
+
*/
|
|
384
|
+
addMorphViolation(node, violations, sourceFile, message) {
|
|
385
|
+
try {
|
|
386
|
+
const start = node.getStart();
|
|
387
|
+
const lineAndColumn = sourceFile.getLineAndColumnAtPos(start);
|
|
388
|
+
const source = node.getText();
|
|
389
|
+
|
|
390
|
+
violations.push({
|
|
391
|
+
ruleId: this.ruleId,
|
|
392
|
+
source: source,
|
|
393
|
+
category: this.category,
|
|
394
|
+
line: lineAndColumn.line,
|
|
395
|
+
column: lineAndColumn.column,
|
|
396
|
+
message: `Insecure session cookie: ${message}`,
|
|
397
|
+
severity: "error",
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
401
|
+
console.log(
|
|
402
|
+
`🔍 [S031] Symbol: Added violation at line ${lineAndColumn.line}`
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
} catch (error) {
|
|
406
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
407
|
+
console.error(
|
|
408
|
+
`🔍 [S031] Symbol: Error adding violation:`,
|
|
409
|
+
error.message
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Visit AST nodes recursively
|
|
417
|
+
*/
|
|
418
|
+
visitNode(node, violations, sourceFile) {
|
|
419
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
420
|
+
const nodeKind = ts.SyntaxKind[node.kind];
|
|
421
|
+
if (
|
|
422
|
+
nodeKind &&
|
|
423
|
+
(nodeKind.includes("Call") || nodeKind.includes("Property"))
|
|
424
|
+
) {
|
|
425
|
+
console.log(`🔍 [S031] Symbol: Visiting ${nodeKind} node`);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Check for cookie setting method calls
|
|
430
|
+
if (ts.isCallExpression(node)) {
|
|
431
|
+
this.checkCookieMethodCall(node, violations, sourceFile);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Check for property assignments (e.g., response.cookie = ...)
|
|
435
|
+
if (ts.isPropertyAssignment(node) || ts.isBinaryExpression(node)) {
|
|
436
|
+
this.checkCookiePropertyAssignment(node, violations, sourceFile);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Continue traversing
|
|
440
|
+
ts.forEachChild(node, (child) => {
|
|
441
|
+
this.visitNode(child, violations, sourceFile);
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Check cookie method calls for security flags
|
|
447
|
+
*/
|
|
448
|
+
checkCookieMethodCall(callNode, violations, sourceFile) {
|
|
449
|
+
const methodName = this.getMethodName(callNode);
|
|
450
|
+
|
|
451
|
+
// Get line number for debugging
|
|
452
|
+
let lineNumber = "unknown";
|
|
453
|
+
try {
|
|
454
|
+
const start = sourceFile.getLineAndCharacterOfPosition(
|
|
455
|
+
callNode.getStart(sourceFile)
|
|
456
|
+
);
|
|
457
|
+
lineNumber = start.line + 1;
|
|
458
|
+
} catch (error) {
|
|
459
|
+
// Ignore line number errors
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
463
|
+
console.log(
|
|
464
|
+
`🔍 [S031] Symbol: Line ${lineNumber} - Method call detected: "${methodName}"`
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (!this.cookieMethods.includes(methodName)) {
|
|
469
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
470
|
+
console.log(
|
|
471
|
+
`🔍 [S031] Symbol: Line ${lineNumber} - Method "${methodName}" not in cookieMethods list`
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Special handling for setHeader("Set-Cookie", [...]) pattern
|
|
478
|
+
if (methodName === "setHeader") {
|
|
479
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
480
|
+
console.log(
|
|
481
|
+
`🔍 [S031] Symbol: Line ${lineNumber} - Special setHeader handling triggered`
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
this.checkSetHeaderCookiesTS(callNode, violations, sourceFile);
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Skip middleware setup patterns
|
|
489
|
+
const callText = callNode.getText();
|
|
490
|
+
if (this.isMiddlewareSetup(callText, methodName)) {
|
|
491
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
492
|
+
console.log(
|
|
493
|
+
`🔍 [S031] Symbol: Line ${lineNumber} - Skipping middleware setup for "${methodName}"`
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Check if this is setting a session-related cookie
|
|
500
|
+
const cookieName = this.extractCookieName(callNode);
|
|
501
|
+
|
|
502
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
503
|
+
console.log(`🔍 [S031] Symbol: Extracted cookie name: "${cookieName}"`);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (!this.isSessionCookie(cookieName, callNode)) {
|
|
507
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
508
|
+
console.log(
|
|
509
|
+
`🔍 [S031] Symbol: Cookie "${cookieName}" not identified as session cookie`
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
516
|
+
console.log(
|
|
517
|
+
`🔍 [S031] Symbol: Cookie "${cookieName}" IS a session cookie, checking secure flag...`
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Check for secure flag in options
|
|
522
|
+
const hasSecureFlag = this.checkSecureFlag(callNode);
|
|
523
|
+
|
|
524
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
525
|
+
console.log(
|
|
526
|
+
`🔍 [S031] Symbol: Secure flag check result: ${hasSecureFlag}`
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (!hasSecureFlag) {
|
|
531
|
+
// Improve message for session middleware
|
|
532
|
+
let violationMessage;
|
|
533
|
+
if (methodName === "session" && (!cookieName || cookieName === "null")) {
|
|
534
|
+
violationMessage = `Session middleware missing secure cookie configuration`;
|
|
535
|
+
} else {
|
|
536
|
+
violationMessage = `Session cookie "${
|
|
537
|
+
cookieName || "unknown"
|
|
538
|
+
}" missing Secure flag`;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
this.addViolation(callNode, violations, sourceFile, violationMessage);
|
|
542
|
+
|
|
543
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
544
|
+
console.log(
|
|
545
|
+
`🔍 [S031] Symbol: ⚠️ VIOLATION ADDED: ${violationMessage}`
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
} else {
|
|
549
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
550
|
+
console.log(
|
|
551
|
+
`🔍 [S031] Symbol: ✅ Cookie "${cookieName}" has secure flag`
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Check if this is middleware setup rather than direct cookie setting
|
|
559
|
+
*/
|
|
560
|
+
isMiddlewareSetup(callText, methodName) {
|
|
561
|
+
if (process.env.SUNLINT_DEBUG && methodName === "session") {
|
|
562
|
+
console.log(`🔍 [S031] Symbol: Checking middleware for session call`);
|
|
563
|
+
console.log(`🔍 [S031] Symbol: Full callText: "${callText}"`);
|
|
564
|
+
console.log(
|
|
565
|
+
`🔍 [S031] Symbol: Contains "cookie:": ${callText.includes("cookie:")}`
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// session() calls inside app.use() with proper cookie config can be skipped
|
|
570
|
+
if (methodName === "session" && callText.includes("cookie:")) {
|
|
571
|
+
// Remove comments to avoid false matches
|
|
572
|
+
const codeOnly = callText
|
|
573
|
+
.replace(/\/\/.*$/gm, "")
|
|
574
|
+
.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
575
|
+
|
|
576
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
577
|
+
console.log(`🔍 [S031] Symbol: Code only (no comments): "${codeOnly}"`);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Check if the cookie config has secure: true (in actual code, not comments)
|
|
581
|
+
const cookieConfigMatch = codeOnly.match(/cookie:\s*{[^}]*}/s);
|
|
582
|
+
if (cookieConfigMatch) {
|
|
583
|
+
const cookieConfig = cookieConfigMatch[0];
|
|
584
|
+
|
|
585
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
586
|
+
console.log(
|
|
587
|
+
`🔍 [S031] Symbol: Found cookie config: "${cookieConfig}"`
|
|
588
|
+
);
|
|
589
|
+
console.log(
|
|
590
|
+
`🔍 [S031] Symbol: Contains "secure:": ${cookieConfig.includes(
|
|
591
|
+
"secure:"
|
|
592
|
+
)}`
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
if (
|
|
597
|
+
cookieConfig.includes("secure:") &&
|
|
598
|
+
(cookieConfig.includes("secure: true") ||
|
|
599
|
+
cookieConfig.includes("secure:true"))
|
|
600
|
+
) {
|
|
601
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
602
|
+
console.log(
|
|
603
|
+
`🔍 [S031] Symbol: ✅ Skipping secure session middleware`
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
return true; // Skip secure middleware setup
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
610
|
+
console.log(
|
|
611
|
+
`🔍 [S031] Symbol: ❌ Not skipping - has cookie config but no secure: true`
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
return false; // Don't skip insecure middleware setup
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// For session() without cookie config, check if it's a violation case
|
|
618
|
+
if (methodName === "session") {
|
|
619
|
+
// If it's in app.use() but has no cookie config, it's likely a violation
|
|
620
|
+
if (callText.includes("app.use(") || callText.includes(".use(")) {
|
|
621
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
622
|
+
console.log(
|
|
623
|
+
`🔍 [S031] Symbol: ❌ Not skipping - session middleware without cookie config (violation)`
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
return false; // Don't skip - needs to be checked for missing cookie config
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// Other non-session middleware patterns can be skipped
|
|
631
|
+
const nonSessionMiddlewarePatterns = [
|
|
632
|
+
/middleware.*(?!session)/i, // middleware but not session
|
|
633
|
+
/setup.*(?!session)/i, // setup but not session
|
|
634
|
+
];
|
|
635
|
+
|
|
636
|
+
const shouldSkip = nonSessionMiddlewarePatterns.some((pattern) =>
|
|
637
|
+
pattern.test(callText)
|
|
638
|
+
);
|
|
639
|
+
if (process.env.SUNLINT_DEBUG && shouldSkip) {
|
|
640
|
+
console.log(`🔍 [S031] Symbol: ✅ Skipping non-session middleware`);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
return shouldSkip;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Check property assignments for cookie security
|
|
648
|
+
*/
|
|
649
|
+
checkCookiePropertyAssignment(node, violations, sourceFile) {
|
|
650
|
+
const nodeText = node.getText(sourceFile);
|
|
651
|
+
|
|
652
|
+
// Check for document.cookie assignments
|
|
653
|
+
if (
|
|
654
|
+
nodeText.includes("document.cookie") ||
|
|
655
|
+
nodeText.includes("Set-Cookie")
|
|
656
|
+
) {
|
|
657
|
+
if (
|
|
658
|
+
this.isSessionCookieString(nodeText) &&
|
|
659
|
+
!this.hasSecureInString(nodeText)
|
|
660
|
+
) {
|
|
661
|
+
this.addViolation(
|
|
662
|
+
node,
|
|
663
|
+
violations,
|
|
664
|
+
sourceFile,
|
|
665
|
+
"Session cookie assignment missing Secure flag"
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Extract method name from call expression
|
|
673
|
+
*/
|
|
674
|
+
getMethodName(callNode) {
|
|
675
|
+
if (ts.isPropertyAccessExpression(callNode.expression)) {
|
|
676
|
+
return callNode.expression.name.text;
|
|
677
|
+
}
|
|
678
|
+
if (ts.isIdentifier(callNode.expression)) {
|
|
679
|
+
return callNode.expression.text;
|
|
680
|
+
}
|
|
681
|
+
return "";
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* Extract cookie name from method call
|
|
686
|
+
*/
|
|
687
|
+
extractCookieName(callNode) {
|
|
688
|
+
if (callNode.arguments && callNode.arguments.length > 0) {
|
|
689
|
+
const firstArg = callNode.arguments[0];
|
|
690
|
+
if (ts.isStringLiteral(firstArg)) {
|
|
691
|
+
return firstArg.text;
|
|
692
|
+
}
|
|
693
|
+
if (ts.isIdentifier(firstArg)) {
|
|
694
|
+
return firstArg.text;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
return null;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* Check if cookie name indicates session cookie
|
|
702
|
+
*/
|
|
703
|
+
isSessionCookie(cookieName, callNode) {
|
|
704
|
+
const methodName = this.getMethodName(callNode);
|
|
705
|
+
|
|
706
|
+
if (process.env.SUNLINT_DEBUG && methodName === "session") {
|
|
707
|
+
console.log(
|
|
708
|
+
`🔍 [S031] Symbol: Checking isSessionCookie for session() call with cookieName: "${cookieName}"`
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// For session() method calls, they ARE always session-related
|
|
713
|
+
if (methodName === "session") {
|
|
714
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
715
|
+
console.log(`🔍 [S031] Symbol: ✅ session() IS a session cookie setup`);
|
|
716
|
+
}
|
|
717
|
+
return true;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
if (!cookieName || cookieName === "null" || cookieName === "unknown") {
|
|
721
|
+
// If no explicit name, check call context more carefully
|
|
722
|
+
const callText = callNode.getText();
|
|
723
|
+
|
|
724
|
+
// Skip if it's obviously not a session cookie setting
|
|
725
|
+
if (
|
|
726
|
+
callText.includes(".json(") ||
|
|
727
|
+
callText.includes(".status(") ||
|
|
728
|
+
callText.includes("generateToken") ||
|
|
729
|
+
callText.includes("authenticateUser")
|
|
730
|
+
) {
|
|
731
|
+
return false;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
return this.sessionIndicators.some((indicator) =>
|
|
735
|
+
callText.toLowerCase().includes(indicator.toLowerCase())
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
return this.sessionIndicators.some((indicator) =>
|
|
740
|
+
cookieName.toLowerCase().includes(indicator.toLowerCase())
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* Check if string contains session cookie indicators
|
|
746
|
+
*/
|
|
747
|
+
isSessionCookieString(text) {
|
|
748
|
+
const lowerText = text.toLowerCase();
|
|
749
|
+
return this.sessionIndicators.some((indicator) =>
|
|
750
|
+
lowerText.includes(indicator.toLowerCase())
|
|
751
|
+
);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* Check for Secure flag in method call options
|
|
756
|
+
*/
|
|
757
|
+
checkSecureFlag(callNode) {
|
|
758
|
+
if (!callNode.arguments || callNode.arguments.length < 1) {
|
|
759
|
+
return false;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// For session() middleware, check if cookie config exists and has secure
|
|
763
|
+
const methodName = this.getMethodName(callNode);
|
|
764
|
+
if (methodName === "session") {
|
|
765
|
+
return this.checkSessionMiddlewareSecure(callNode);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// For regular cookie methods, check options object (usually second or third argument)
|
|
769
|
+
if (callNode.arguments.length < 2) {
|
|
770
|
+
return false;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
for (let i = 1; i < callNode.arguments.length; i++) {
|
|
774
|
+
const arg = callNode.arguments[i];
|
|
775
|
+
if (ts.isObjectLiteralExpression(arg)) {
|
|
776
|
+
if (this.hasSecureProperty(arg)) {
|
|
777
|
+
return true;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// Check for config references
|
|
781
|
+
const argText = arg.getText();
|
|
782
|
+
if (this.hasSecureConfigReference(argText)) {
|
|
783
|
+
return true;
|
|
784
|
+
}
|
|
785
|
+
} else {
|
|
786
|
+
// Check if argument references a secure config
|
|
787
|
+
const argText = arg.getText();
|
|
788
|
+
if (this.hasSecureConfigReference(argText)) {
|
|
789
|
+
return true;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
return false;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* Check session middleware for cookie.secure configuration
|
|
799
|
+
*/
|
|
800
|
+
checkSessionMiddlewareSecure(callNode) {
|
|
801
|
+
if (!callNode.arguments || callNode.arguments.length === 0) {
|
|
802
|
+
return false;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// Session config is usually the first argument
|
|
806
|
+
const configArg = callNode.arguments[0];
|
|
807
|
+
if (!ts.isObjectLiteralExpression(configArg)) {
|
|
808
|
+
return false;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// Look for cookie property
|
|
812
|
+
for (const property of configArg.properties) {
|
|
813
|
+
if (ts.isPropertyAssignment(property)) {
|
|
814
|
+
const propName = property.name;
|
|
815
|
+
if (ts.isIdentifier(propName) && propName.text === "cookie") {
|
|
816
|
+
// Check cookie object for secure property
|
|
817
|
+
if (ts.isObjectLiteralExpression(property.initializer)) {
|
|
818
|
+
const cookieObj = property.initializer;
|
|
819
|
+
|
|
820
|
+
// Look for secure property in cookie config
|
|
821
|
+
for (const cookieProp of cookieObj.properties) {
|
|
822
|
+
if (ts.isPropertyAssignment(cookieProp)) {
|
|
823
|
+
const cookiePropName = cookieProp.name;
|
|
824
|
+
if (
|
|
825
|
+
ts.isIdentifier(cookiePropName) &&
|
|
826
|
+
cookiePropName.text === "secure"
|
|
827
|
+
) {
|
|
828
|
+
// Check if secure is true, or a variable (like isProduction)
|
|
829
|
+
const secureValue = cookieProp.initializer;
|
|
830
|
+
if (secureValue.kind === ts.SyntaxKind.TrueKeyword) {
|
|
831
|
+
return true; // explicit secure: true
|
|
832
|
+
}
|
|
833
|
+
if (ts.isIdentifier(secureValue)) {
|
|
834
|
+
// Variable like isProduction - assume secure
|
|
835
|
+
return true;
|
|
836
|
+
}
|
|
837
|
+
if (secureValue.kind === ts.SyntaxKind.FalseKeyword) {
|
|
838
|
+
return false; // explicit secure: false
|
|
839
|
+
}
|
|
840
|
+
// Any other expression - assume secure (conservative)
|
|
841
|
+
return true;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
return false; // cookie config exists but no secure property
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// Check if cookie references a secure config
|
|
849
|
+
const cookieText = property.initializer.getText();
|
|
850
|
+
if (this.hasSecureConfigReference(cookieText)) {
|
|
851
|
+
return true;
|
|
852
|
+
}
|
|
853
|
+
return false; // cookie property exists but not secure
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// No cookie property found = missing cookie config = violation
|
|
859
|
+
return false;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/**
|
|
863
|
+
* Check if text contains secure config references
|
|
864
|
+
*/
|
|
865
|
+
hasSecureConfigReference(text) {
|
|
866
|
+
const secureConfigPatterns = [
|
|
867
|
+
/\bcookieConfig\b/i,
|
|
868
|
+
/\bsecureConfig\b/i,
|
|
869
|
+
/\.\.\..*config/i,
|
|
870
|
+
/secureOptions/i,
|
|
871
|
+
/cookieDefaults/i,
|
|
872
|
+
/httpOnly.*secure/i,
|
|
873
|
+
/secure.*httpOnly/i,
|
|
874
|
+
];
|
|
875
|
+
|
|
876
|
+
return secureConfigPatterns.some((pattern) => pattern.test(text));
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* Check if object has secure property set to true
|
|
881
|
+
*/
|
|
882
|
+
hasSecureProperty(objectNode) {
|
|
883
|
+
for (const property of objectNode.properties) {
|
|
884
|
+
if (ts.isPropertyAssignment(property)) {
|
|
885
|
+
const propName = property.name;
|
|
886
|
+
if (ts.isIdentifier(propName) && propName.text === "secure") {
|
|
887
|
+
// Check if value is true
|
|
888
|
+
if (property.initializer.kind === ts.SyntaxKind.TrueKeyword) {
|
|
889
|
+
return true;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
return false;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
/**
|
|
898
|
+
* Check if string contains Secure flag
|
|
899
|
+
*/
|
|
900
|
+
hasSecureInString(text) {
|
|
901
|
+
const securePatterns = [
|
|
902
|
+
/secure\s*[:=]\s*true/i,
|
|
903
|
+
/;\s*secure\s*[;\s]/i,
|
|
904
|
+
/;\s*secure$/i,
|
|
905
|
+
/secure\s*=\s*true/i,
|
|
906
|
+
];
|
|
907
|
+
|
|
908
|
+
return securePatterns.some((pattern) => pattern.test(text));
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
/**
|
|
912
|
+
* Add violation to results
|
|
913
|
+
*/
|
|
914
|
+
addViolation(node, violations, sourceFile, message) {
|
|
915
|
+
const start = sourceFile.getLineAndCharacterOfPosition(
|
|
916
|
+
node.getStart(sourceFile)
|
|
917
|
+
);
|
|
918
|
+
const source = node.getText(sourceFile);
|
|
919
|
+
|
|
920
|
+
violations.push({
|
|
921
|
+
ruleId: this.ruleId,
|
|
922
|
+
source: source,
|
|
923
|
+
category: this.category,
|
|
924
|
+
line: start.line + 1,
|
|
925
|
+
column: start.character + 1,
|
|
926
|
+
message: `Insecure session cookie: ${message}`,
|
|
927
|
+
severity: "error",
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/**
|
|
932
|
+
* Check setHeader("Set-Cookie", [...]) pattern for insecure session cookies (TypeScript compiler API)
|
|
933
|
+
*/
|
|
934
|
+
checkSetHeaderCookiesTS(callNode, violations, sourceFile) {
|
|
935
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
936
|
+
console.log(`🔍 [S031] Symbol: checkSetHeaderCookiesTS called`);
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
try {
|
|
940
|
+
const args = callNode.arguments;
|
|
941
|
+
if (!args || args.length < 2) {
|
|
942
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
943
|
+
console.log(
|
|
944
|
+
`🔍 [S031] Symbol: setHeader insufficient args: ${
|
|
945
|
+
args?.length || 0
|
|
946
|
+
}`
|
|
947
|
+
);
|
|
948
|
+
}
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
// Check if first argument is "Set-Cookie"
|
|
953
|
+
const firstArg = args[0];
|
|
954
|
+
let headerName = "";
|
|
955
|
+
if (firstArg.kind === ts.SyntaxKind.StringLiteral) {
|
|
956
|
+
headerName = firstArg.text;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
if (headerName !== "Set-Cookie") {
|
|
960
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
961
|
+
console.log(
|
|
962
|
+
`🔍 [S031] Symbol: Not Set-Cookie header: "${headerName}"`
|
|
963
|
+
);
|
|
964
|
+
}
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
969
|
+
console.log(
|
|
970
|
+
`🔍 [S031] Symbol: Set-Cookie header detected, checking array...`
|
|
971
|
+
);
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// Get the array of cookie strings from second argument
|
|
975
|
+
const secondArg = args[1];
|
|
976
|
+
if (!secondArg) {
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// Parse cookie strings from array
|
|
981
|
+
const cookieStrings = this.extractCookieStringsFromArrayTS(secondArg);
|
|
982
|
+
|
|
983
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
984
|
+
console.log(
|
|
985
|
+
`🔍 [S031] Symbol: Extracted ${cookieStrings.length} cookie strings`
|
|
986
|
+
);
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
for (const cookieString of cookieStrings) {
|
|
990
|
+
const cookieName = this.extractCookieNameFromString(cookieString);
|
|
991
|
+
|
|
992
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
993
|
+
console.log(
|
|
994
|
+
`🔍 [S031] Symbol: Checking cookie "${cookieName}" from string: "${cookieString}"`
|
|
995
|
+
);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
if (this.isSessionCookieName(cookieName)) {
|
|
999
|
+
const hasSecure = cookieString.toLowerCase().includes("secure");
|
|
1000
|
+
|
|
1001
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
1002
|
+
console.log(
|
|
1003
|
+
`🔍 [S031] Symbol: Session cookie "${cookieName}" has secure: ${hasSecure}`
|
|
1004
|
+
);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
if (!hasSecure) {
|
|
1008
|
+
this.addViolation(
|
|
1009
|
+
callNode,
|
|
1010
|
+
violations,
|
|
1011
|
+
sourceFile,
|
|
1012
|
+
`Session cookie "${cookieName}" in Set-Cookie header missing Secure attribute`
|
|
1013
|
+
);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
} catch (error) {
|
|
1018
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
1019
|
+
console.log(
|
|
1020
|
+
`🔍 [S031] Symbol: Error checking setHeader cookies:`,
|
|
1021
|
+
error.message
|
|
1022
|
+
);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
/**
|
|
1028
|
+
* Extract cookie strings from array literal (TypeScript compiler API)
|
|
1029
|
+
*/
|
|
1030
|
+
extractCookieStringsFromArrayTS(arrayNode) {
|
|
1031
|
+
const cookieStrings = [];
|
|
1032
|
+
|
|
1033
|
+
try {
|
|
1034
|
+
if (arrayNode.kind === ts.SyntaxKind.ArrayLiteralExpression) {
|
|
1035
|
+
const elements = arrayNode.elements;
|
|
1036
|
+
|
|
1037
|
+
for (const element of elements) {
|
|
1038
|
+
let cookieString = "";
|
|
1039
|
+
|
|
1040
|
+
if (element.kind === ts.SyntaxKind.StringLiteral) {
|
|
1041
|
+
cookieString = element.text;
|
|
1042
|
+
} else if (
|
|
1043
|
+
element.kind === ts.SyntaxKind.TemplateExpression ||
|
|
1044
|
+
element.kind === ts.SyntaxKind.NoSubstitutionTemplateLiteral
|
|
1045
|
+
) {
|
|
1046
|
+
// Handle template literals
|
|
1047
|
+
cookieString = element.getText();
|
|
1048
|
+
// Remove backticks
|
|
1049
|
+
cookieString = cookieString.replace(/^`/, "").replace(/`$/, "");
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
if (cookieString) {
|
|
1053
|
+
cookieStrings.push(cookieString);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
} catch (error) {
|
|
1058
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
1059
|
+
console.log(
|
|
1060
|
+
`🔍 [S031] Symbol: Error extracting cookie strings:`,
|
|
1061
|
+
error.message
|
|
1062
|
+
);
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
return cookieStrings;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
/**
|
|
1070
|
+
* Check if cookie name indicates session cookie (for setHeader pattern)
|
|
1071
|
+
*/
|
|
1072
|
+
isSessionCookieName(cookieName) {
|
|
1073
|
+
if (!cookieName) return false;
|
|
1074
|
+
|
|
1075
|
+
const lowerName = cookieName.toLowerCase();
|
|
1076
|
+
|
|
1077
|
+
// Check against session cookie patterns
|
|
1078
|
+
return this.sessionIndicators.some((keyword) =>
|
|
1079
|
+
lowerName.includes(keyword.toLowerCase())
|
|
1080
|
+
);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
module.exports = S031SymbolBasedAnalyzer;
|