@sun-asterisk/sunlint 1.3.2 → 1.3.3
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 +38 -0
- package/README.md +5 -3
- package/config/rules/enhanced-rules-registry.json +144 -33
- package/core/analysis-orchestrator.js +167 -42
- package/core/auto-performance-manager.js +243 -0
- package/core/cli-action-handler.js +9 -1
- 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/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,732 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S033 Symbol-Based Analyzer - Set SameSite attribute for Session Cookies
|
|
3
|
+
* Uses TypeScript compiler API for semantic analysis
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const ts = require("typescript");
|
|
7
|
+
|
|
8
|
+
class S033SymbolBasedAnalyzer {
|
|
9
|
+
constructor(semanticEngine = null) {
|
|
10
|
+
this.semanticEngine = semanticEngine;
|
|
11
|
+
this.ruleId = "S033";
|
|
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
|
+
"next-auth",
|
|
29
|
+
"user_session",
|
|
30
|
+
"api_session",
|
|
31
|
+
"login_session",
|
|
32
|
+
"auth_token",
|
|
33
|
+
"csrf_token",
|
|
34
|
+
"refresh_token",
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
// Cookie methods that need security checking (enhanced with framework support)
|
|
38
|
+
this.cookieMethods = [
|
|
39
|
+
"setCookie", // Nuxt.js H3
|
|
40
|
+
"useCookie", // Nuxt.js composable
|
|
41
|
+
"cookie", // Express.js res.cookie
|
|
42
|
+
"set", // Next.js response.cookies.set
|
|
43
|
+
"append", // Express.js res.append
|
|
44
|
+
"session", // Session middleware
|
|
45
|
+
"setHeader", // Node.js res.setHeader
|
|
46
|
+
"writeHead", // Node.js res.writeHead
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
// Acceptable SameSite values
|
|
50
|
+
this.acceptableValues = ["strict", "lax", "none"];
|
|
51
|
+
this.recommendedValues = ["strict", "lax"];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Initialize analyzer with semantic engine
|
|
56
|
+
*/
|
|
57
|
+
async initialize(semanticEngine) {
|
|
58
|
+
this.semanticEngine = semanticEngine;
|
|
59
|
+
if (this.verbose) {
|
|
60
|
+
console.log(`🔍 [${this.ruleId}] Symbol: Semantic engine initialized`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async analyze(filePath) {
|
|
65
|
+
if (this.verbose) {
|
|
66
|
+
console.log(
|
|
67
|
+
`🔍 [${this.ruleId}] Symbol: Starting analysis for ${filePath}`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!this.semanticEngine) {
|
|
72
|
+
if (this.verbose) {
|
|
73
|
+
console.log(
|
|
74
|
+
`🔍 [${this.ruleId}] Symbol: No semantic engine available, skipping`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const sourceFile = this.semanticEngine.getSourceFile(filePath);
|
|
82
|
+
if (!sourceFile) {
|
|
83
|
+
if (this.verbose) {
|
|
84
|
+
console.log(
|
|
85
|
+
`🔍 [${this.ruleId}] Symbol: No source file found, trying ts-morph fallback`
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
return await this.analyzeTsMorph(filePath);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (this.verbose) {
|
|
92
|
+
console.log(`🔧 [${this.ruleId}] Source file found, analyzing...`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return await this.analyzeSourceFile(sourceFile, filePath);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
if (this.verbose) {
|
|
98
|
+
console.log(
|
|
99
|
+
`🔍 [${this.ruleId}] Symbol: Error in analysis:`,
|
|
100
|
+
error.message
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async analyzeTsMorph(filePath) {
|
|
108
|
+
try {
|
|
109
|
+
if (this.verbose) {
|
|
110
|
+
console.log(`🔍 [${this.ruleId}] Symbol: Starting ts-morph analysis`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const { Project } = require("ts-morph");
|
|
114
|
+
const project = new Project();
|
|
115
|
+
const sourceFile = project.addSourceFileAtPath(filePath);
|
|
116
|
+
|
|
117
|
+
return await this.analyzeSourceFile(sourceFile, filePath);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (this.verbose) {
|
|
120
|
+
console.log(
|
|
121
|
+
`🔍 [${this.ruleId}] Symbol: ts-morph analysis failed:`,
|
|
122
|
+
error.message
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async analyzeSourceFile(sourceFile, filePath) {
|
|
130
|
+
const violations = [];
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
if (this.verbose) {
|
|
134
|
+
console.log(`🔍 [${this.ruleId}] Symbol: Starting ts-morph analysis`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const callExpressions = sourceFile.getDescendantsOfKind
|
|
138
|
+
? sourceFile.getDescendantsOfKind(
|
|
139
|
+
require("typescript").SyntaxKind.CallExpression
|
|
140
|
+
)
|
|
141
|
+
: [];
|
|
142
|
+
|
|
143
|
+
if (this.verbose) {
|
|
144
|
+
console.log(
|
|
145
|
+
`🔍 [${this.ruleId}] Symbol: Found ${callExpressions.length} call expressions`
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
for (const callNode of callExpressions) {
|
|
150
|
+
try {
|
|
151
|
+
if (this.verbose) {
|
|
152
|
+
const expressionText = callNode.getExpression().getText();
|
|
153
|
+
console.log(
|
|
154
|
+
`🔍 [${this.ruleId}] Symbol: Expression kind: ${callNode
|
|
155
|
+
.getExpression()
|
|
156
|
+
.getKind()}, text: "${expressionText.substring(0, 50)}..."`
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Handle property access expressions (e.g., res.cookie, res.setHeader)
|
|
161
|
+
if (
|
|
162
|
+
callNode.getExpression().getKind() ===
|
|
163
|
+
require("typescript").SyntaxKind.PropertyAccessExpression
|
|
164
|
+
) {
|
|
165
|
+
const methodName = callNode.getExpression().getName();
|
|
166
|
+
|
|
167
|
+
if (this.verbose) {
|
|
168
|
+
console.log(
|
|
169
|
+
`🔍 [${this.ruleId}] Symbol: PropertyAccess method name: "${methodName}"`
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (this.verbose) {
|
|
174
|
+
console.log(
|
|
175
|
+
`🔍 [${this.ruleId}] Symbol: ts-morph Method call detected: "${methodName}"`
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (!this.cookieMethods.includes(methodName)) {
|
|
180
|
+
if (this.verbose) {
|
|
181
|
+
console.log(
|
|
182
|
+
`🔍 [${this.ruleId}] Symbol: Method "${methodName}" not in cookieMethods list`
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (this.verbose) {
|
|
189
|
+
console.log(
|
|
190
|
+
`🔍 [${this.ruleId}] Symbol: Method "${methodName}" found in cookieMethods, proceeding...`
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Special handling for setHeader method
|
|
195
|
+
if (methodName === "setHeader") {
|
|
196
|
+
const violation = this.analyzeSetHeaderCall(callNode, sourceFile);
|
|
197
|
+
if (violation) {
|
|
198
|
+
violations.push(violation);
|
|
199
|
+
}
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Analyze cookie method calls
|
|
204
|
+
const violation = this.analyzeCookieCall(
|
|
205
|
+
callNode,
|
|
206
|
+
sourceFile,
|
|
207
|
+
methodName
|
|
208
|
+
);
|
|
209
|
+
if (violation) {
|
|
210
|
+
violations.push(violation);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
} catch (error) {
|
|
214
|
+
if (this.verbose) {
|
|
215
|
+
console.log(
|
|
216
|
+
`🔍 [${this.ruleId}] Symbol: Error analyzing call expression:`,
|
|
217
|
+
error.message
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (this.verbose) {
|
|
224
|
+
console.log(
|
|
225
|
+
`🔍 [${this.ruleId}] Symbol: Analysis completed. Found ${violations.length} violations`
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return violations;
|
|
230
|
+
} catch (error) {
|
|
231
|
+
if (this.verbose) {
|
|
232
|
+
console.log(
|
|
233
|
+
`🔍 [${this.ruleId}] Symbol: Error in source file analysis:`,
|
|
234
|
+
error.message
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
return [];
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
analyzeSetHeaderCall(callNode, sourceFile) {
|
|
242
|
+
try {
|
|
243
|
+
const args = callNode.getArguments();
|
|
244
|
+
|
|
245
|
+
if (args.length < 2) return null;
|
|
246
|
+
|
|
247
|
+
const headerName = args[0].getText().replace(/['"]/g, "");
|
|
248
|
+
if (headerName.toLowerCase() !== "set-cookie") return null;
|
|
249
|
+
|
|
250
|
+
const line = sourceFile.getLineAndColumnAtPos(callNode.getStart()).line;
|
|
251
|
+
|
|
252
|
+
if (this.verbose) {
|
|
253
|
+
console.log(
|
|
254
|
+
`🔍 [${this.ruleId}] Symbol: Special setHeader handling triggered for line ${line}`
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const cookieValue = args[1].getText();
|
|
259
|
+
|
|
260
|
+
// Check for SameSite in Set-Cookie header
|
|
261
|
+
if (
|
|
262
|
+
this.isSessionCookieHeader(cookieValue) &&
|
|
263
|
+
!this.hasSameSiteAttribute(cookieValue)
|
|
264
|
+
) {
|
|
265
|
+
const cookieName = this.extractCookieNameFromHeader(cookieValue);
|
|
266
|
+
return this.createViolation(
|
|
267
|
+
sourceFile,
|
|
268
|
+
callNode,
|
|
269
|
+
`Session cookie "${cookieName}" in Set-Cookie header missing SameSite attribute`
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return null;
|
|
274
|
+
} catch (error) {
|
|
275
|
+
if (this.verbose) {
|
|
276
|
+
console.log(
|
|
277
|
+
`🔍 [${this.ruleId}] Symbol: Error analyzing setHeader:`,
|
|
278
|
+
error.message
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
analyzeCookieCall(callNode, sourceFile, methodName) {
|
|
286
|
+
try {
|
|
287
|
+
const args = callNode.getArguments();
|
|
288
|
+
|
|
289
|
+
if (args.length < 1) return null;
|
|
290
|
+
|
|
291
|
+
// Get cookie name using enhanced method
|
|
292
|
+
const cookieName = this.extractMorphCookieName(callNode);
|
|
293
|
+
|
|
294
|
+
if (this.verbose) {
|
|
295
|
+
console.log(
|
|
296
|
+
`🔍 [${
|
|
297
|
+
this.ruleId
|
|
298
|
+
}] Symbol: Cookie "${cookieName}" session check: ${this.isSessionCookie(
|
|
299
|
+
cookieName
|
|
300
|
+
)}`
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Only analyze session cookies
|
|
305
|
+
if (!this.isSessionCookie(cookieName)) {
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Get framework context for better messaging
|
|
310
|
+
const framework = this.detectFramework(callNode, sourceFile);
|
|
311
|
+
|
|
312
|
+
// Check if SameSite is configured based on method type
|
|
313
|
+
const hasSameSite = this.checkSameSiteInCall(callNode, methodName);
|
|
314
|
+
|
|
315
|
+
if (!hasSameSite) {
|
|
316
|
+
const frameworkMessage =
|
|
317
|
+
framework !== "Framework" ? ` (${framework})` : "";
|
|
318
|
+
return this.createViolation(
|
|
319
|
+
sourceFile,
|
|
320
|
+
callNode,
|
|
321
|
+
`Session cookie "${cookieName}"${frameworkMessage} missing SameSite attribute`
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return null;
|
|
326
|
+
} catch (error) {
|
|
327
|
+
if (this.verbose) {
|
|
328
|
+
console.log(
|
|
329
|
+
`🔍 [${this.ruleId}] Symbol: Error analyzing cookie call:`,
|
|
330
|
+
error.message
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Check for SameSite attribute in cookie call
|
|
339
|
+
*/
|
|
340
|
+
checkSameSiteInCall(callNode, methodName) {
|
|
341
|
+
try {
|
|
342
|
+
const args = callNode.getArguments();
|
|
343
|
+
|
|
344
|
+
// For setCookie(event, name, value, options), options is at index 3
|
|
345
|
+
let optionsIndex = 2;
|
|
346
|
+
if (methodName === "setCookie" && args.length >= 4) {
|
|
347
|
+
optionsIndex = 3; // Options argument for setCookie
|
|
348
|
+
} else if (methodName === "useCookie" && args.length >= 2) {
|
|
349
|
+
optionsIndex = 1; // Options argument for useCookie
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Check if options argument exists
|
|
353
|
+
if (args.length <= optionsIndex) {
|
|
354
|
+
// No options object provided
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const optionsArg = args[optionsIndex];
|
|
359
|
+
return this.checkSameSiteInOptions(optionsArg, callNode);
|
|
360
|
+
} catch (error) {
|
|
361
|
+
if (this.verbose) {
|
|
362
|
+
console.log(
|
|
363
|
+
`🔍 [${this.ruleId}] Symbol: Error checking SameSite in call:`,
|
|
364
|
+
error.message
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
checkSameSiteInOptions(optionsArg, callNode) {
|
|
372
|
+
try {
|
|
373
|
+
const SyntaxKind = require("typescript").SyntaxKind;
|
|
374
|
+
|
|
375
|
+
if (optionsArg.getKind() === SyntaxKind.ObjectLiteralExpression) {
|
|
376
|
+
let text = optionsArg.getText();
|
|
377
|
+
|
|
378
|
+
if (this.verbose) {
|
|
379
|
+
console.log(
|
|
380
|
+
`🔍 [${
|
|
381
|
+
this.ruleId
|
|
382
|
+
}] Symbol: Checking object literal: ${text.substring(0, 200)}...`
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Remove comments to avoid false positives
|
|
387
|
+
const textWithoutComments = text
|
|
388
|
+
.replace(/\/\/.*$/gm, "")
|
|
389
|
+
.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
390
|
+
|
|
391
|
+
// Check for explicitly configured SameSite
|
|
392
|
+
if (this.hasSameSiteInText(textWithoutComments)) {
|
|
393
|
+
if (this.verbose) {
|
|
394
|
+
console.log(
|
|
395
|
+
`🔍 [${this.ruleId}] Symbol: SameSite found in object literal`
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
return true;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Check for spread elements within the object literal
|
|
402
|
+
const hasSpreadElements = text.includes("...");
|
|
403
|
+
if (hasSpreadElements) {
|
|
404
|
+
if (this.verbose) {
|
|
405
|
+
console.log(
|
|
406
|
+
`🔍 [${this.ruleId}] Symbol: Object literal contains spread elements, checking each...`
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const spreadMatches = text.match(/\.\.\.([^,}]+)/g);
|
|
411
|
+
if (spreadMatches) {
|
|
412
|
+
for (const spreadMatch of spreadMatches) {
|
|
413
|
+
const reference = spreadMatch.replace(/^\.\.\./g, "").trim();
|
|
414
|
+
if (this.verbose) {
|
|
415
|
+
console.log(
|
|
416
|
+
`🔍 [${this.ruleId}] Symbol: Checking spread reference: ${reference}`
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (this.isSecureConfigReference(reference, callNode)) {
|
|
421
|
+
if (this.verbose) {
|
|
422
|
+
console.log(
|
|
423
|
+
`🔍 [${this.ruleId}] Symbol: ✅ Secure spread reference detected: ${reference}`
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
return true;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (this.verbose) {
|
|
433
|
+
console.log(
|
|
434
|
+
`🔍 [${this.ruleId}] Symbol: Object literal missing SameSite and no secure spreads`
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
return false;
|
|
438
|
+
} else if (
|
|
439
|
+
optionsArg.getKind() === SyntaxKind.Identifier ||
|
|
440
|
+
optionsArg.getKind() === SyntaxKind.PropertyAccessExpression
|
|
441
|
+
) {
|
|
442
|
+
const argText = optionsArg.getText();
|
|
443
|
+
if (this.verbose) {
|
|
444
|
+
console.log(
|
|
445
|
+
`🔍 [${this.ruleId}] Symbol: Found reference: ${argText}`
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (this.isSecureConfigReference(argText, callNode)) {
|
|
450
|
+
if (this.verbose) {
|
|
451
|
+
console.log(
|
|
452
|
+
`🔍 [${this.ruleId}] Symbol: ✅ Secure config reference detected: ${argText}`
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
return true;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return false;
|
|
460
|
+
} catch (error) {
|
|
461
|
+
if (this.verbose) {
|
|
462
|
+
console.log(
|
|
463
|
+
`🔍 [${this.ruleId}] Symbol: Error checking SameSite in options:`,
|
|
464
|
+
error.message
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
return false;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Check if reference points to secure configuration
|
|
473
|
+
*/
|
|
474
|
+
isSecureConfigReference(argText, callNode) {
|
|
475
|
+
try {
|
|
476
|
+
const sourceFile = callNode.getSourceFile();
|
|
477
|
+
const fileText = sourceFile.getFullText();
|
|
478
|
+
|
|
479
|
+
// Handle this.cookieConfig pattern
|
|
480
|
+
if (argText.includes("cookieConfig") || argText.includes("config")) {
|
|
481
|
+
const configName = argText.split(".").pop();
|
|
482
|
+
|
|
483
|
+
// Look for the exact config definition and check if it contains sameSite
|
|
484
|
+
const configDefPattern = new RegExp(
|
|
485
|
+
`(?:private|public|readonly|const|let|var)\\s+(?:readonly\\s+)?${configName}\\s*=\\s*{[^}]*}`,
|
|
486
|
+
"gis"
|
|
487
|
+
);
|
|
488
|
+
|
|
489
|
+
const configMatch = fileText.match(configDefPattern);
|
|
490
|
+
|
|
491
|
+
if (this.verbose) {
|
|
492
|
+
console.log(
|
|
493
|
+
`🔍 [${this.ruleId}] Symbol: Looking for config definition of "${configName}"`
|
|
494
|
+
);
|
|
495
|
+
console.log(
|
|
496
|
+
`🔍 [${this.ruleId}] Symbol: Config match found:`,
|
|
497
|
+
configMatch ? configMatch[0] : "none"
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (configMatch) {
|
|
502
|
+
let configContent = configMatch[0];
|
|
503
|
+
|
|
504
|
+
// Remove comments to avoid false positives
|
|
505
|
+
configContent = configContent
|
|
506
|
+
.replace(/\/\/.*$/gm, "")
|
|
507
|
+
.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
508
|
+
|
|
509
|
+
const hasSameSite = this.hasSameSiteInText(configContent);
|
|
510
|
+
|
|
511
|
+
if (this.verbose) {
|
|
512
|
+
console.log(
|
|
513
|
+
`🔍 [${this.ruleId}] Symbol: Config content (comments removed):`,
|
|
514
|
+
configContent
|
|
515
|
+
);
|
|
516
|
+
console.log(
|
|
517
|
+
`🔍 [${this.ruleId}] Symbol: SameSite found:`,
|
|
518
|
+
hasSameSite
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
return hasSameSite;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if (this.verbose) {
|
|
526
|
+
console.log(
|
|
527
|
+
`🔍 [${this.ruleId}] Symbol: No config definition found for "${configName}"`
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
return false;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Handle variable references
|
|
535
|
+
const varPattern = new RegExp(
|
|
536
|
+
`(?:const|let|var)\\s+${argText}\\s*=\\s*{[^}]*sameSite\\s*:`,
|
|
537
|
+
"i"
|
|
538
|
+
);
|
|
539
|
+
return varPattern.test(fileText);
|
|
540
|
+
} catch (error) {
|
|
541
|
+
if (this.verbose) {
|
|
542
|
+
console.log(
|
|
543
|
+
`🔍 [${this.ruleId}] Symbol: Error checking config reference:`,
|
|
544
|
+
error.message
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
return false;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
hasSameSiteInText(text) {
|
|
552
|
+
// Check for sameSite with acceptable values
|
|
553
|
+
const sameSitePatterns = [
|
|
554
|
+
/sameSite\s*:\s*['"](strict|lax|none)['"]|sameSite\s*:\s*(strict|lax|none)/i,
|
|
555
|
+
/sameSite\s*:\s*.*\?\s*['"](strict|lax|none)['"]\s*:\s*['"](strict|lax|none)['"]/i, // Ternary operator
|
|
556
|
+
/sameSite\s*:\s*.*\?\s*(strict|lax|none)\s*:\s*(strict|lax|none)/i, // Ternary without quotes
|
|
557
|
+
];
|
|
558
|
+
|
|
559
|
+
return sameSitePatterns.some((pattern) => pattern.test(text));
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
hasSameSiteAttribute(cookieValue) {
|
|
563
|
+
// Check for SameSite in Set-Cookie header
|
|
564
|
+
const sameSitePattern = /SameSite=(Strict|Lax|None)/i;
|
|
565
|
+
return sameSitePattern.test(cookieValue);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
isSessionCookie(cookieName) {
|
|
569
|
+
const name = cookieName.toLowerCase();
|
|
570
|
+
return this.sessionIndicators.some((indicator) =>
|
|
571
|
+
name.includes(indicator.toLowerCase())
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
isSessionCookieHeader(cookieValue) {
|
|
576
|
+
// Extract cookie name from Set-Cookie header value
|
|
577
|
+
const nameMatch = cookieValue.match(/^[^=]+/);
|
|
578
|
+
if (!nameMatch) return false;
|
|
579
|
+
|
|
580
|
+
const cookieName = nameMatch[0].replace(/['"]/g, "").trim();
|
|
581
|
+
return this.isSessionCookie(cookieName);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
extractCookieNameFromHeader(cookieValue) {
|
|
585
|
+
const nameMatch = cookieValue.match(/^[^=]+/);
|
|
586
|
+
return nameMatch ? nameMatch[0].replace(/['"]/g, "").trim() : "unknown";
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
createViolation(sourceFile, callNode, message) {
|
|
590
|
+
try {
|
|
591
|
+
const start = callNode.getStart();
|
|
592
|
+
const lineAndChar = sourceFile.getLineAndColumnAtPos(start);
|
|
593
|
+
|
|
594
|
+
return {
|
|
595
|
+
rule: this.ruleId,
|
|
596
|
+
source: sourceFile.getFilePath(),
|
|
597
|
+
category: this.category,
|
|
598
|
+
line: lineAndChar.line,
|
|
599
|
+
column: lineAndChar.column,
|
|
600
|
+
message: `Insecure session cookie: ${message}`,
|
|
601
|
+
severity: "error",
|
|
602
|
+
};
|
|
603
|
+
} catch (error) {
|
|
604
|
+
if (this.verbose) {
|
|
605
|
+
console.log(
|
|
606
|
+
`🔍 [${this.ruleId}] Symbol: Error creating violation:`,
|
|
607
|
+
error.message
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
return null;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Detect framework from method call context
|
|
616
|
+
*/
|
|
617
|
+
detectFramework(callNode, sourceFile) {
|
|
618
|
+
const callText = callNode.getText();
|
|
619
|
+
const fileContent = sourceFile.getFullText();
|
|
620
|
+
|
|
621
|
+
// Check imports to detect framework
|
|
622
|
+
if (
|
|
623
|
+
fileContent.includes("@nestjs/common") ||
|
|
624
|
+
fileContent.includes("@Res()")
|
|
625
|
+
) {
|
|
626
|
+
return "NestJS";
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if (
|
|
630
|
+
fileContent.includes("next/server") ||
|
|
631
|
+
fileContent.includes("NextResponse") ||
|
|
632
|
+
fileContent.includes("NextAuth")
|
|
633
|
+
) {
|
|
634
|
+
return "Next.js";
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
if (
|
|
638
|
+
callText.includes("useCookie") ||
|
|
639
|
+
fileContent.includes("defineEventHandler") ||
|
|
640
|
+
fileContent.includes("setCookie")
|
|
641
|
+
) {
|
|
642
|
+
return "Nuxt.js";
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
return "Framework";
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Enhanced cookie name extraction with framework support
|
|
650
|
+
*/
|
|
651
|
+
extractMorphCookieName(callNode) {
|
|
652
|
+
try {
|
|
653
|
+
const args = callNode.getArguments();
|
|
654
|
+
if (args && args.length > 0) {
|
|
655
|
+
const methodName = this.getMorphMethodName(callNode);
|
|
656
|
+
|
|
657
|
+
// Handle setCookie(event, "cookieName", "value", options) pattern
|
|
658
|
+
if (methodName === "setCookie" && args.length >= 2) {
|
|
659
|
+
const secondArg = args[1]; // Cookie name is second argument
|
|
660
|
+
if (secondArg && secondArg.getText) {
|
|
661
|
+
const text = secondArg.getText();
|
|
662
|
+
return text.replace(/['"]/g, ""); // Remove quotes
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Handle standard cookie methods (cookieName is first argument)
|
|
667
|
+
const firstArg = args[0];
|
|
668
|
+
if (firstArg && firstArg.getText) {
|
|
669
|
+
const text = firstArg.getText();
|
|
670
|
+
return text.replace(/['"]/g, ""); // Remove quotes
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
} catch (error) {
|
|
674
|
+
if (this.verbose) {
|
|
675
|
+
console.log(
|
|
676
|
+
`🔍 [${this.ruleId}] Symbol: Error extracting cookie name:`,
|
|
677
|
+
error.message
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
return null;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* Enhanced method name detection with framework support
|
|
686
|
+
*/
|
|
687
|
+
getMorphMethodName(callNode) {
|
|
688
|
+
try {
|
|
689
|
+
const expression = callNode.getExpression();
|
|
690
|
+
const ts = require("typescript");
|
|
691
|
+
|
|
692
|
+
// Handle property access expressions (obj.method)
|
|
693
|
+
if (expression.getKind() === ts.SyntaxKind.PropertyAccessExpression) {
|
|
694
|
+
const propertyName = expression.getNameNode().getText();
|
|
695
|
+
|
|
696
|
+
// Check for chained method calls like response.cookies.set
|
|
697
|
+
if (propertyName === "set" || propertyName === "cookie") {
|
|
698
|
+
const objectExpression = expression.getExpression();
|
|
699
|
+
if (
|
|
700
|
+
objectExpression.getKind() ===
|
|
701
|
+
ts.SyntaxKind.PropertyAccessExpression
|
|
702
|
+
) {
|
|
703
|
+
const parentProperty = objectExpression.getNameNode().getText();
|
|
704
|
+
if (parentProperty === "cookies") {
|
|
705
|
+
return "set"; // For cookies.set()
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
return propertyName;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
return propertyName;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// Handle direct function calls
|
|
715
|
+
if (expression.getKind() === ts.SyntaxKind.Identifier) {
|
|
716
|
+
return expression.getText();
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
return "";
|
|
720
|
+
} catch (error) {
|
|
721
|
+
if (this.verbose) {
|
|
722
|
+
console.log(
|
|
723
|
+
`🔍 [${this.ruleId}] Symbol: Error getting method name:`,
|
|
724
|
+
error.message
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
return "";
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
module.exports = S033SymbolBasedAnalyzer;
|