@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.
- package/CHANGELOG.md +38 -0
- package/config/defaults/default.json +2 -1
- package/config/rule-analysis-strategies.js +20 -0
- package/config/rules/enhanced-rules-registry.json +190 -35
- package/core/file-targeting-service.js +83 -7
- package/package.json +1 -1
- package/rules/common/C065_one_behavior_per_test/analyzer.js +851 -0
- package/rules/common/C065_one_behavior_per_test/config.json +95 -0
- package/rules/security/S037_cache_headers/README.md +128 -0
- package/rules/security/S037_cache_headers/analyzer.js +263 -0
- package/rules/security/S037_cache_headers/config.json +50 -0
- package/rules/security/S037_cache_headers/regex-based-analyzer.js +463 -0
- package/rules/security/S037_cache_headers/symbol-based-analyzer.js +546 -0
- package/rules/security/S038_no_version_headers/README.md +234 -0
- package/rules/security/S038_no_version_headers/analyzer.js +262 -0
- package/rules/security/S038_no_version_headers/config.json +49 -0
- package/rules/security/S038_no_version_headers/regex-based-analyzer.js +339 -0
- package/rules/security/S038_no_version_headers/symbol-based-analyzer.js +375 -0
- package/rules/security/S039_no_session_tokens_in_url/README.md +198 -0
- package/rules/security/S039_no_session_tokens_in_url/analyzer.js +262 -0
- package/rules/security/S039_no_session_tokens_in_url/config.json +92 -0
- package/rules/security/S039_no_session_tokens_in_url/regex-based-analyzer.js +337 -0
- package/rules/security/S039_no_session_tokens_in_url/symbol-based-analyzer.js +436 -0
- package/rules/security/S049_short_validity_tokens/analyzer.js +175 -0
- package/rules/security/S049_short_validity_tokens/config.json +124 -0
- package/rules/security/S049_short_validity_tokens/regex-based-analyzer.js +295 -0
- package/rules/security/S049_short_validity_tokens/symbol-based-analyzer.js +389 -0
- package/rules/security/S051_password_length_policy/analyzer.js +410 -0
- package/rules/security/S051_password_length_policy/config.json +83 -0
- package/rules/security/S052_weak_otp_entropy/analyzer.js +403 -0
- package/rules/security/S052_weak_otp_entropy/config.json +57 -0
- package/rules/security/S054_no_default_accounts/README.md +129 -0
- package/rules/security/S054_no_default_accounts/analyzer.js +792 -0
- package/rules/security/S054_no_default_accounts/config.json +101 -0
- package/rules/security/S056_log_injection_protection/analyzer.js +242 -0
- package/rules/security/S056_log_injection_protection/config.json +148 -0
- package/rules/security/S056_log_injection_protection/regex-based-analyzer.js +120 -0
- package/rules/security/S056_log_injection_protection/symbol-based-analyzer.js +287 -0
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S037 Symbol-Based Analyzer - Configure comprehensive cache headers
|
|
3
|
+
* Enhanced to analyze per route handler instead of per file
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class S037SymbolBasedAnalyzer {
|
|
7
|
+
constructor(semanticEngine) {
|
|
8
|
+
this.ruleId = "S037";
|
|
9
|
+
this.semanticEngine = semanticEngine;
|
|
10
|
+
this.requiredDirectives = ["no-store", "no-cache", "must-revalidate"];
|
|
11
|
+
this.sensitiveIndicators =
|
|
12
|
+
/\b(session|auth(?:enticate|orization)?|token|jwt|csrf|login|logout|banking|crypto|salary|ssn|social[-_]?security|medical|prescription|biometric|audit|tax|legal|contract|personal[-_]?data|identity|finance|wallet|private[-_]?key|secret|sensitive[-_]?data|confidential|admin[-_]?panel|payment[-_]?process|credit[-_]?card|oauth|password|reset)\b/i;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async initialize() {}
|
|
16
|
+
|
|
17
|
+
analyze(sourceFile, filePath) {
|
|
18
|
+
const violations = [];
|
|
19
|
+
|
|
20
|
+
// Skip files that are unlikely to be route handlers
|
|
21
|
+
const skipPatterns = [
|
|
22
|
+
/\.dto\.ts$/,
|
|
23
|
+
/\.interface\.ts$/,
|
|
24
|
+
/\.module\.ts$/,
|
|
25
|
+
/\.service\.spec\.ts$/,
|
|
26
|
+
/\.controller\.spec\.ts$/,
|
|
27
|
+
/\.spec\.ts$/,
|
|
28
|
+
/\.test\.ts$/,
|
|
29
|
+
/\.d\.ts$/,
|
|
30
|
+
/\.types\.ts$/,
|
|
31
|
+
/\.constants?.ts$/,
|
|
32
|
+
/\.config\.ts$/,
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
const shouldSkip = skipPatterns.some((pattern) => pattern.test(filePath));
|
|
36
|
+
if (shouldSkip) {
|
|
37
|
+
return violations;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const { SyntaxKind } = require("ts-morph");
|
|
42
|
+
|
|
43
|
+
// Find all function expressions and arrow functions that could be route handlers
|
|
44
|
+
const routeHandlers = [];
|
|
45
|
+
|
|
46
|
+
// Express route patterns: app.get("/path", (req, res) => {...})
|
|
47
|
+
const callExpressions = sourceFile.getDescendantsOfKind(
|
|
48
|
+
SyntaxKind.CallExpression
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
for (const call of callExpressions) {
|
|
52
|
+
const expression = call.getExpression();
|
|
53
|
+
|
|
54
|
+
// Check for Express route methods
|
|
55
|
+
if (/\.(get|post|put|delete|patch|use)$/.test(expression.getText())) {
|
|
56
|
+
const args = call.getArguments();
|
|
57
|
+
if (args.length >= 2) {
|
|
58
|
+
const lastArg = args[args.length - 1];
|
|
59
|
+
// The last argument should be the handler function
|
|
60
|
+
if (
|
|
61
|
+
lastArg.getKind() === SyntaxKind.ArrowFunction ||
|
|
62
|
+
lastArg.getKind() === SyntaxKind.FunctionExpression
|
|
63
|
+
) {
|
|
64
|
+
routeHandlers.push({
|
|
65
|
+
handler: lastArg,
|
|
66
|
+
routeCall: call,
|
|
67
|
+
type: "express",
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Next.js export functions
|
|
75
|
+
const exportAssignments = sourceFile.getDescendantsOfKind(
|
|
76
|
+
SyntaxKind.ExportAssignment
|
|
77
|
+
);
|
|
78
|
+
const exportDeclarations = sourceFile.getDescendantsOfKind(
|
|
79
|
+
SyntaxKind.ExportDeclaration
|
|
80
|
+
);
|
|
81
|
+
const functionDeclarations = sourceFile.getDescendantsOfKind(
|
|
82
|
+
SyntaxKind.FunctionDeclaration
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
for (const func of functionDeclarations) {
|
|
86
|
+
const name = func.getName();
|
|
87
|
+
if (name && /^(GET|POST|PUT|DELETE|PATCH|handler)$/.test(name)) {
|
|
88
|
+
routeHandlers.push({
|
|
89
|
+
handler: func,
|
|
90
|
+
type: "nextjs",
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// NestJS Controller methods with decorators
|
|
96
|
+
const methodDeclarations = sourceFile.getDescendantsOfKind(
|
|
97
|
+
SyntaxKind.MethodDeclaration
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
for (const method of methodDeclarations) {
|
|
101
|
+
const decorators = method.getDecorators();
|
|
102
|
+
const hasRouteDecorator = decorators.some((d) => {
|
|
103
|
+
const decoratorName = d.getName();
|
|
104
|
+
return ["Get", "Post", "Put", "Delete", "Patch"].includes(
|
|
105
|
+
decoratorName
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
if (hasRouteDecorator) {
|
|
110
|
+
routeHandlers.push({
|
|
111
|
+
handler: method,
|
|
112
|
+
type: "nestjs",
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Nuxt.js defineEventHandler patterns
|
|
118
|
+
for (const call of callExpressions) {
|
|
119
|
+
const expression = call.getExpression();
|
|
120
|
+
if (expression.getKind() === SyntaxKind.Identifier) {
|
|
121
|
+
const identifier = expression.asKindOrThrow(SyntaxKind.Identifier);
|
|
122
|
+
if (identifier.getText() === "defineEventHandler") {
|
|
123
|
+
// Find the arrow function or function parameter
|
|
124
|
+
const args = call.getArguments();
|
|
125
|
+
if (args.length > 0) {
|
|
126
|
+
const firstArg = args[0];
|
|
127
|
+
if (
|
|
128
|
+
firstArg.getKind() === SyntaxKind.ArrowFunction ||
|
|
129
|
+
firstArg.getKind() === SyntaxKind.FunctionExpression
|
|
130
|
+
) {
|
|
131
|
+
routeHandlers.push({
|
|
132
|
+
handler: firstArg,
|
|
133
|
+
type: "nuxtjs",
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Nuxt.js named exports (GET, POST, etc.)
|
|
142
|
+
const nuxtExportAssignments = sourceFile.getDescendantsOfKind(
|
|
143
|
+
SyntaxKind.ExportAssignment
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
for (const exportAssign of nuxtExportAssignments) {
|
|
147
|
+
const expr = exportAssign.getExpression();
|
|
148
|
+
if (expr && expr.getKind() === SyntaxKind.CallExpression) {
|
|
149
|
+
const callExpr = expr.asKindOrThrow(SyntaxKind.CallExpression);
|
|
150
|
+
const callExpression = callExpr.getExpression();
|
|
151
|
+
if (callExpression.getKind() === SyntaxKind.Identifier) {
|
|
152
|
+
const identifier = callExpression.asKindOrThrow(
|
|
153
|
+
SyntaxKind.Identifier
|
|
154
|
+
);
|
|
155
|
+
if (identifier.getText() === "defineEventHandler") {
|
|
156
|
+
const args = callExpr.getArguments();
|
|
157
|
+
if (args.length > 0) {
|
|
158
|
+
const firstArg = args[0];
|
|
159
|
+
if (
|
|
160
|
+
firstArg.getKind() === SyntaxKind.ArrowFunction ||
|
|
161
|
+
firstArg.getKind() === SyntaxKind.FunctionExpression
|
|
162
|
+
) {
|
|
163
|
+
routeHandlers.push({
|
|
164
|
+
handler: firstArg,
|
|
165
|
+
type: "nuxtjs",
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const variableDeclarations = sourceFile.getDescendantsOfKind(
|
|
175
|
+
SyntaxKind.VariableDeclaration
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
for (const varDecl of variableDeclarations) {
|
|
179
|
+
const name = varDecl.getName();
|
|
180
|
+
if (name && /^(GET|POST|PUT|DELETE|PATCH)$/.test(name)) {
|
|
181
|
+
const initializer = varDecl.getInitializer();
|
|
182
|
+
if (
|
|
183
|
+
initializer &&
|
|
184
|
+
initializer.getKind() === SyntaxKind.CallExpression
|
|
185
|
+
) {
|
|
186
|
+
const callExpr = initializer.asKindOrThrow(
|
|
187
|
+
SyntaxKind.CallExpression
|
|
188
|
+
);
|
|
189
|
+
const callExpression = callExpr.getExpression();
|
|
190
|
+
if (callExpression.getKind() === SyntaxKind.Identifier) {
|
|
191
|
+
const identifier = callExpression.asKindOrThrow(
|
|
192
|
+
SyntaxKind.Identifier
|
|
193
|
+
);
|
|
194
|
+
if (identifier.getText() === "defineEventHandler") {
|
|
195
|
+
const args = callExpr.getArguments();
|
|
196
|
+
if (args.length > 0) {
|
|
197
|
+
const firstArg = args[0];
|
|
198
|
+
if (
|
|
199
|
+
firstArg.getKind() === SyntaxKind.ArrowFunction ||
|
|
200
|
+
firstArg.getKind() === SyntaxKind.FunctionExpression
|
|
201
|
+
) {
|
|
202
|
+
routeHandlers.push({
|
|
203
|
+
handler: firstArg,
|
|
204
|
+
type: "nuxtjs",
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Analyze each route handler
|
|
215
|
+
if (process.env.SUNLINT_DEBUG)
|
|
216
|
+
console.log(
|
|
217
|
+
`🔧 [S037-Symbol] Found ${routeHandlers.length} route handlers`
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
for (const route of routeHandlers) {
|
|
221
|
+
const handlerViolations = this.analyzeRouteHandler(route);
|
|
222
|
+
violations.push(...handlerViolations);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Only analyze actual route handlers - no file-level checking
|
|
226
|
+
// File-level checking removed to prevent false positives on DTO, module, service files
|
|
227
|
+
} catch (err) {
|
|
228
|
+
// Fallback - don't report errors from symbol analysis
|
|
229
|
+
console.warn(`Symbol analysis failed for ${filePath}:`, err.message);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return violations;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
analyzeRouteHandler(route) {
|
|
236
|
+
const violations = [];
|
|
237
|
+
const { handler, routeCall, type } = route;
|
|
238
|
+
|
|
239
|
+
// Check if route contains sensitive indicators
|
|
240
|
+
const handlerText = handler.getFullText();
|
|
241
|
+
const routeText = routeCall ? routeCall.getFullText() : handlerText;
|
|
242
|
+
|
|
243
|
+
// Check for secure middleware (skip analysis if present)
|
|
244
|
+
if (/secureNoCache|noCache|antiCache/.test(routeText)) {
|
|
245
|
+
return violations; // Middleware handles headers
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const isSensitive =
|
|
249
|
+
this.sensitiveIndicators.test(handlerText) ||
|
|
250
|
+
this.sensitiveIndicators.test(routeText);
|
|
251
|
+
|
|
252
|
+
// Find header setting calls within this handler
|
|
253
|
+
const headerState = this.findHeadersInNode(handler);
|
|
254
|
+
|
|
255
|
+
// Only analyze if route is sensitive OR headers are being set
|
|
256
|
+
const touchedHeaders =
|
|
257
|
+
headerState.hasCC || headerState.hasPragma || headerState.hasExpires;
|
|
258
|
+
if (!isSensitive && !touchedHeaders) {
|
|
259
|
+
return violations; // Skip non-sensitive routes without headers
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Check for missing headers
|
|
263
|
+
const missing = [];
|
|
264
|
+
if (!headerState.hasCC) missing.push("Cache-Control");
|
|
265
|
+
if (!headerState.hasPragma) missing.push("Pragma");
|
|
266
|
+
if (!headerState.hasExpires) missing.push("Expires");
|
|
267
|
+
|
|
268
|
+
if (missing.length > 0) {
|
|
269
|
+
violations.push({
|
|
270
|
+
ruleId: this.ruleId,
|
|
271
|
+
message: `Handler missing anti-cache headers: ${missing.join(
|
|
272
|
+
", "
|
|
273
|
+
)} (${type})`,
|
|
274
|
+
severity: "warning",
|
|
275
|
+
line: handler.getStartLineNumber(),
|
|
276
|
+
column: 1,
|
|
277
|
+
});
|
|
278
|
+
} else {
|
|
279
|
+
// Validate Cache-Control directives
|
|
280
|
+
if (headerState.ccValues.length > 0) {
|
|
281
|
+
const combined = headerState.ccValues.join(",").toLowerCase();
|
|
282
|
+
const missingDir = this.requiredDirectives.filter(
|
|
283
|
+
(d) => !combined.includes(d)
|
|
284
|
+
);
|
|
285
|
+
if (missingDir.length > 0) {
|
|
286
|
+
violations.push({
|
|
287
|
+
ruleId: this.ruleId,
|
|
288
|
+
message: `Cache-Control missing directives: ${missingDir.join(
|
|
289
|
+
", "
|
|
290
|
+
)} (${type})`,
|
|
291
|
+
severity: "warning",
|
|
292
|
+
line: handler.getStartLineNumber(),
|
|
293
|
+
column: 1,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return violations;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
findHeadersInNode(node) {
|
|
303
|
+
const { SyntaxKind } = require("ts-morph");
|
|
304
|
+
const headerState = {
|
|
305
|
+
hasCC: false,
|
|
306
|
+
hasPragma: false,
|
|
307
|
+
hasExpires: false,
|
|
308
|
+
ccValues: [],
|
|
309
|
+
expiresValue: null,
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
// Find all call expressions within this node
|
|
313
|
+
const calls = node.getDescendantsOfKind(SyntaxKind.CallExpression);
|
|
314
|
+
const newExpressions = node.getDescendantsOfKind(SyntaxKind.NewExpression);
|
|
315
|
+
|
|
316
|
+
// Check for Next.js new Response() constructor with headers
|
|
317
|
+
for (const newExpr of newExpressions) {
|
|
318
|
+
const identifier = newExpr.getExpression();
|
|
319
|
+
if (identifier.getText() === "Response") {
|
|
320
|
+
const args = newExpr.getArguments();
|
|
321
|
+
if (args.length >= 2) {
|
|
322
|
+
const optionsArg = args[1];
|
|
323
|
+
if (optionsArg.getKind() === SyntaxKind.ObjectLiteralExpression) {
|
|
324
|
+
const headersProp = optionsArg.getProperty("headers");
|
|
325
|
+
if (
|
|
326
|
+
headersProp &&
|
|
327
|
+
headersProp.getKind() === SyntaxKind.PropertyAssignment
|
|
328
|
+
) {
|
|
329
|
+
const headersValue = headersProp.getInitializer();
|
|
330
|
+
if (
|
|
331
|
+
headersValue &&
|
|
332
|
+
headersValue.getKind() === SyntaxKind.ObjectLiteralExpression
|
|
333
|
+
) {
|
|
334
|
+
this.parseHeadersObject(headersValue, headerState);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
for (const call of calls) {
|
|
343
|
+
const expression = call.getExpression();
|
|
344
|
+
const methodName = expression.getText();
|
|
345
|
+
|
|
346
|
+
// Look for header setting methods: Express (res.set, res.setHeader), NestJS (res.header)
|
|
347
|
+
if (
|
|
348
|
+
/\.set(Header)?$/.test(methodName) ||
|
|
349
|
+
/setHeader$/.test(methodName) ||
|
|
350
|
+
/\.set$/.test(methodName) ||
|
|
351
|
+
/\.header$/.test(methodName)
|
|
352
|
+
) {
|
|
353
|
+
const args = call.getArguments();
|
|
354
|
+
if (args.length >= 2) {
|
|
355
|
+
const headerArg = args[0];
|
|
356
|
+
const valueArg = args[1];
|
|
357
|
+
const headerName = headerArg.getText().replace(/['"`]/g, "");
|
|
358
|
+
const headerValue = valueArg.getText().replace(/['"`]/g, "");
|
|
359
|
+
|
|
360
|
+
if (headerName.toLowerCase() === "cache-control") {
|
|
361
|
+
headerState.hasCC = true;
|
|
362
|
+
headerState.ccValues.push(headerValue);
|
|
363
|
+
} else if (headerName.toLowerCase() === "pragma") {
|
|
364
|
+
headerState.hasPragma = true;
|
|
365
|
+
} else if (headerName.toLowerCase() === "expires") {
|
|
366
|
+
headerState.hasExpires = this.isValidExpires(headerValue);
|
|
367
|
+
headerState.expiresValue = headerValue;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Check for Nuxt.js setHeader(event, name, value) pattern
|
|
373
|
+
if (methodName === "setHeader") {
|
|
374
|
+
const args = call.getArguments();
|
|
375
|
+
if (args.length >= 3) {
|
|
376
|
+
// First arg should be 'event', second is header name, third is value
|
|
377
|
+
const eventArg = args[0];
|
|
378
|
+
const headerArg = args[1];
|
|
379
|
+
const valueArg = args[2];
|
|
380
|
+
|
|
381
|
+
if (eventArg.getText() === "event") {
|
|
382
|
+
const headerName = headerArg.getText().replace(/['"`]/g, "");
|
|
383
|
+
const headerValue = valueArg.getText().replace(/['"`]/g, "");
|
|
384
|
+
|
|
385
|
+
if (headerName.toLowerCase() === "cache-control") {
|
|
386
|
+
headerState.hasCC = true;
|
|
387
|
+
headerState.ccValues.push(headerValue);
|
|
388
|
+
} else if (headerName.toLowerCase() === "pragma") {
|
|
389
|
+
headerState.hasPragma = true;
|
|
390
|
+
} else if (headerName.toLowerCase() === "expires") {
|
|
391
|
+
headerState.hasExpires = this.isValidExpires(headerValue);
|
|
392
|
+
headerState.expiresValue = headerValue;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Check for Next.js Response constructor with headers object
|
|
399
|
+
if (
|
|
400
|
+
call.getExpression().getText() === "Response" ||
|
|
401
|
+
call.getExpression().getKind() === SyntaxKind.NewExpression
|
|
402
|
+
) {
|
|
403
|
+
const args = call.getArguments();
|
|
404
|
+
if (args.length >= 2) {
|
|
405
|
+
const optionsArg = args[1];
|
|
406
|
+
if (optionsArg.getKind() === SyntaxKind.ObjectLiteralExpression) {
|
|
407
|
+
const headersProp = optionsArg.getProperty("headers");
|
|
408
|
+
if (
|
|
409
|
+
headersProp &&
|
|
410
|
+
headersProp.getKind() === SyntaxKind.PropertyAssignment
|
|
411
|
+
) {
|
|
412
|
+
const headersValue = headersProp.getInitializer();
|
|
413
|
+
if (
|
|
414
|
+
headersValue &&
|
|
415
|
+
headersValue.getKind() === SyntaxKind.ObjectLiteralExpression
|
|
416
|
+
) {
|
|
417
|
+
this.parseHeadersObject(headersValue, headerState);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Check for bulk header setting: res.set({...})
|
|
425
|
+
if (/\.set$/.test(methodName)) {
|
|
426
|
+
const args = call.getArguments();
|
|
427
|
+
if (args.length === 1) {
|
|
428
|
+
const objArg = args[0];
|
|
429
|
+
if (objArg.getKind() === SyntaxKind.ObjectLiteralExpression) {
|
|
430
|
+
const props = objArg.getProperties();
|
|
431
|
+
for (const prop of props) {
|
|
432
|
+
if (prop.getKind() === SyntaxKind.PropertyAssignment) {
|
|
433
|
+
const name = prop.getName().replace(/['"`]/g, "").toLowerCase();
|
|
434
|
+
const value =
|
|
435
|
+
prop.getInitializer()?.getText().replace(/['"`]/g, "") || "";
|
|
436
|
+
|
|
437
|
+
if (name === "cache-control") {
|
|
438
|
+
headerState.hasCC = true;
|
|
439
|
+
headerState.ccValues.push(value);
|
|
440
|
+
} else if (name === "pragma") {
|
|
441
|
+
headerState.hasPragma = true;
|
|
442
|
+
} else if (name === "expires") {
|
|
443
|
+
headerState.hasExpires = true;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return headerState;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
isValidExpires(expiresValue) {
|
|
456
|
+
// Remove quotes and trim
|
|
457
|
+
const value = expiresValue.replace(/['"`]/g, "").trim();
|
|
458
|
+
|
|
459
|
+
// Valid immediate expiry values
|
|
460
|
+
if (value === "0" || value === "-1") {
|
|
461
|
+
return true;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Check for past dates (Thu, 01 Jan 1970...)
|
|
465
|
+
if (value.includes("1970") || value.includes("Jan 1970")) {
|
|
466
|
+
return true;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Check for invalid formats
|
|
470
|
+
if (
|
|
471
|
+
value === "never" ||
|
|
472
|
+
value === "invalid-date" ||
|
|
473
|
+
value === "invalid-date-format"
|
|
474
|
+
) {
|
|
475
|
+
return false;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Try parsing as date to check if it's a future date
|
|
479
|
+
try {
|
|
480
|
+
const date = new Date(value);
|
|
481
|
+
if (isNaN(date.getTime())) {
|
|
482
|
+
return false; // Invalid date format
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// If it's a future date (after current time), it's invalid for cache prevention
|
|
486
|
+
const now = new Date();
|
|
487
|
+
if (date > now) {
|
|
488
|
+
return false; // Future dates allow caching
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
return true; // Past date is valid
|
|
492
|
+
} catch (e) {
|
|
493
|
+
return false; // Invalid format
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
parseHeadersObject(headersObject, headerState) {
|
|
498
|
+
const { SyntaxKind } = require("ts-morph");
|
|
499
|
+
const props = headersObject.getProperties();
|
|
500
|
+
|
|
501
|
+
for (const prop of props) {
|
|
502
|
+
if (prop.getKind() === SyntaxKind.PropertyAssignment) {
|
|
503
|
+
const name = prop.getName().replace(/['"`]/g, "").toLowerCase();
|
|
504
|
+
const value =
|
|
505
|
+
prop.getInitializer()?.getText().replace(/['"`]/g, "") || "";
|
|
506
|
+
|
|
507
|
+
if (name === "cache-control") {
|
|
508
|
+
headerState.hasCC = true;
|
|
509
|
+
headerState.ccValues.push(value);
|
|
510
|
+
} else if (name === "pragma") {
|
|
511
|
+
headerState.hasPragma = true;
|
|
512
|
+
} else if (name === "expires") {
|
|
513
|
+
headerState.hasExpires = true;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
analyzeFileLevel(sourceFile) {
|
|
520
|
+
const violations = [];
|
|
521
|
+
const headerState = this.findHeadersInNode(sourceFile);
|
|
522
|
+
|
|
523
|
+
const missing = [];
|
|
524
|
+
if (!headerState.hasCC) missing.push("Cache-Control");
|
|
525
|
+
if (!headerState.hasPragma) missing.push("Pragma");
|
|
526
|
+
if (!headerState.hasExpires) missing.push("Expires");
|
|
527
|
+
|
|
528
|
+
if (missing.length > 0) {
|
|
529
|
+
violations.push({
|
|
530
|
+
ruleId: this.ruleId,
|
|
531
|
+
message: `File contains sensitive data but missing anti-cache headers: ${missing.join(
|
|
532
|
+
", "
|
|
533
|
+
)}`,
|
|
534
|
+
severity: "warning",
|
|
535
|
+
line: 1,
|
|
536
|
+
column: 1,
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
return violations;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
cleanup() {}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
module.exports = S037SymbolBasedAnalyzer;
|