@sun-asterisk/sunlint 1.3.6 → 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 +76 -1
- package/config/defaults/default.json +2 -1
- package/config/rule-analysis-strategies.js +20 -0
- package/config/rules/enhanced-rules-registry.json +230 -43
- package/core/analysis-orchestrator.js +9 -5
- package/core/file-targeting-service.js +83 -7
- package/core/performance-optimizer.js +8 -2
- 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/common/C073_validate_required_config_on_startup/README.md +110 -0
- package/rules/common/C073_validate_required_config_on_startup/analyzer.js +770 -0
- package/rules/common/C073_validate_required_config_on_startup/config.json +46 -0
- package/rules/common/C073_validate_required_config_on_startup/symbol-based-analyzer.js +370 -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
- package/rules/security/S057_utc_logging/README.md +152 -0
- package/rules/security/S057_utc_logging/analyzer.js +457 -0
- package/rules/security/S057_utc_logging/config.json +105 -0
- package/rules/security/S058_no_ssrf/README.md +180 -0
- package/rules/security/S058_no_ssrf/analyzer.js +403 -0
- package/rules/security/S058_no_ssrf/config.json +125 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "C073",
|
|
3
|
+
"name": "C073_validate_required_config_on_startup",
|
|
4
|
+
"category": "configuration",
|
|
5
|
+
"description": "C073 - Validate mandatory configuration at startup and fail fast on invalid/missing values.",
|
|
6
|
+
"severity": "error",
|
|
7
|
+
"enabled": true,
|
|
8
|
+
"semantic": { "enabled": true, "priority": "high", "fallback": "heuristic" },
|
|
9
|
+
"patterns": {
|
|
10
|
+
"include": ["**/*.ts","**/*.tsx","**/*.js","**/*.jsx","**/*.java","**/*.go"],
|
|
11
|
+
"exclude": ["**/*.test.*","**/*.spec.*","**/__tests__/**","**/fixtures/**","**/examples/**"]
|
|
12
|
+
},
|
|
13
|
+
"options": {
|
|
14
|
+
"configModules": {
|
|
15
|
+
"typescript": ["src/config/**","**/config/**","**/bootstrap/**","**/main.ts"],
|
|
16
|
+
"java": ["**/config/**","**/Configuration/**","**/Application.java","**/*Application.java"],
|
|
17
|
+
"go": ["cmd/**","**/config/**","**/main.go"]
|
|
18
|
+
},
|
|
19
|
+
"envAccessors": {
|
|
20
|
+
"typescript": ["process.env.*"],
|
|
21
|
+
"java": ["System.getenv(*)","System.getProperty(*)"],
|
|
22
|
+
"go": ["os.Getenv(*)"]
|
|
23
|
+
},
|
|
24
|
+
"schemaDetectors": {
|
|
25
|
+
"typescript": ["zod","joi","yup","envalid","dotenv-safe","class-validator"],
|
|
26
|
+
"java": ["@ConfigurationProperties","@Validated","jakarta.validation","hibernate.validator"],
|
|
27
|
+
"go": ["github.com/kelseyhightower/envconfig","github.com/spf13/viper"]
|
|
28
|
+
},
|
|
29
|
+
"failFastSignals": {
|
|
30
|
+
"typescript": ["throw new Error(*)","process.exit(1)"],
|
|
31
|
+
"java": ["throw new RuntimeException(*)","SpringApplication.exit(*)","System.exit(1)"],
|
|
32
|
+
"go": ["log.Fatal(*)","panic(*)","os.Exit(1)"]
|
|
33
|
+
},
|
|
34
|
+
"dangerousDefaults": ["|| ''","|| 0","|| 'http://localhost'","?: ''","?: 0"],
|
|
35
|
+
"thresholds": {
|
|
36
|
+
"maxEnvReadsOutsideConfig": 3
|
|
37
|
+
},
|
|
38
|
+
"policy": {
|
|
39
|
+
"requireSchemaOrExplicitChecks": true,
|
|
40
|
+
"requireFailFast": true,
|
|
41
|
+
"forbidEnvReadsOutsideConfig": true,
|
|
42
|
+
"flagDangerousDefaults": true,
|
|
43
|
+
"requireStartupConnectivityChecks": true
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
const { traverse } = require('@babel/traverse');
|
|
2
|
+
const t = require('@babel/types');
|
|
3
|
+
|
|
4
|
+
class C073SymbolBasedAnalyzer {
|
|
5
|
+
constructor(options = {}) {
|
|
6
|
+
this.options = options;
|
|
7
|
+
this.configModules = options.configModules || {};
|
|
8
|
+
this.envAccessors = options.envAccessors || {};
|
|
9
|
+
this.schemaDetectors = options.schemaDetectors || {};
|
|
10
|
+
this.failFastSignals = options.failFastSignals || {};
|
|
11
|
+
this.dangerousDefaults = options.dangerousDefaults || [];
|
|
12
|
+
this.policy = options.policy || {};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
analyze(ast, filePath, content) {
|
|
16
|
+
const analysis = {
|
|
17
|
+
envAccess: [],
|
|
18
|
+
schemaValidation: [],
|
|
19
|
+
failFastMechanisms: [],
|
|
20
|
+
dangerousDefaults: [],
|
|
21
|
+
connectivityChecks: [],
|
|
22
|
+
imports: [],
|
|
23
|
+
configValidation: false,
|
|
24
|
+
hasFailFast: false
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const language = this.detectLanguageFromPath(filePath);
|
|
28
|
+
|
|
29
|
+
// Analyze both config files and service files for different patterns
|
|
30
|
+
const isConfigFile = this.isConfigOrStartupFile(filePath, language);
|
|
31
|
+
|
|
32
|
+
// Traverse AST to collect information
|
|
33
|
+
traverse(ast, {
|
|
34
|
+
// Track imports for schema validation libraries
|
|
35
|
+
ImportDeclaration: (path) => {
|
|
36
|
+
const source = path.node.source.value;
|
|
37
|
+
analysis.imports.push(source);
|
|
38
|
+
|
|
39
|
+
// Check for schema validation libraries
|
|
40
|
+
if (this.isSchemaValidationLibrary(source, language)) {
|
|
41
|
+
analysis.schemaValidation.push({
|
|
42
|
+
library: source,
|
|
43
|
+
line: path.node.loc?.start.line || 1
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
// Track environment variable access
|
|
49
|
+
MemberExpression: (path) => {
|
|
50
|
+
if (this.isEnvAccess(path.node, language)) {
|
|
51
|
+
const envProperty = this.getEnvProperty(path.node);
|
|
52
|
+
analysis.envAccess.push({
|
|
53
|
+
variable: envProperty,
|
|
54
|
+
match: `process.env.${envProperty}`,
|
|
55
|
+
line: path.node.loc?.start.line || 1,
|
|
56
|
+
hasDefault: this.hasDefaultValue(path.parent)
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
// Track call expressions for multiple purposes
|
|
62
|
+
CallExpression: (path) => {
|
|
63
|
+
const node = path.node;
|
|
64
|
+
|
|
65
|
+
// Check for require statements
|
|
66
|
+
if (t.isIdentifier(node.callee, { name: 'require' })) {
|
|
67
|
+
const source = node.arguments[0]?.value;
|
|
68
|
+
if (source && this.isSchemaValidationLibrary(source, language)) {
|
|
69
|
+
analysis.schemaValidation.push({
|
|
70
|
+
library: source,
|
|
71
|
+
line: node.loc?.start.line || 1
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Check for fail-fast calls (process.exit, throw, etc.)
|
|
77
|
+
if (this.isFailFastCall(node, language)) {
|
|
78
|
+
analysis.failFastMechanisms.push({
|
|
79
|
+
type: this.getFailFastType(node),
|
|
80
|
+
line: node.loc?.start.line || 1
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Check for connectivity patterns (ping, connect, health check)
|
|
85
|
+
if (this.isConnectivityCheck(node)) {
|
|
86
|
+
analysis.connectivityChecks.push({
|
|
87
|
+
method: this.getConnectivityMethod(node),
|
|
88
|
+
line: node.loc?.start.line || 1
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
// Track dangerous default patterns
|
|
94
|
+
LogicalExpression: (path) => {
|
|
95
|
+
if (this.isDangerousDefault(path.node)) {
|
|
96
|
+
analysis.dangerousDefaults.push({
|
|
97
|
+
operator: path.node.operator,
|
|
98
|
+
pattern: this.getDangerousDefaultPattern(path.node),
|
|
99
|
+
line: path.node.loc?.start.line || 1
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
// Track conditional expressions with dangerous defaults
|
|
105
|
+
ConditionalExpression: (path) => {
|
|
106
|
+
if (this.isDangerousConditionalDefault(path.node)) {
|
|
107
|
+
analysis.dangerousDefaults.push({
|
|
108
|
+
type: 'conditional',
|
|
109
|
+
pattern: this.getDangerousDefaultPattern(path.node),
|
|
110
|
+
line: path.node.loc?.start.line || 1
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
// Check for validation patterns
|
|
116
|
+
IfStatement: (path) => {
|
|
117
|
+
if (this.isConfigValidation(path.node, analysis.envAccess)) {
|
|
118
|
+
analysis.configValidation = true;
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
// Check for throw statements with configuration errors
|
|
123
|
+
ThrowStatement: (path) => {
|
|
124
|
+
if (this.isConfigRelatedError(path.node)) {
|
|
125
|
+
analysis.failFastMechanisms.push({
|
|
126
|
+
type: 'throw',
|
|
127
|
+
line: path.node.loc?.start.line || 1
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
return analysis;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
detectLanguageFromPath(filePath) {
|
|
137
|
+
const ext = filePath.split('.').pop()?.toLowerCase();
|
|
138
|
+
if (['ts', 'tsx', 'js', 'jsx'].includes(ext)) return 'typescript';
|
|
139
|
+
if (['java'].includes(ext)) return 'java';
|
|
140
|
+
if (['go'].includes(ext)) return 'go';
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
isConfigOrStartupFile(filePath, language) {
|
|
145
|
+
const configPatterns = this.configModules[language] || [];
|
|
146
|
+
return configPatterns.some(pattern => {
|
|
147
|
+
const globPattern = pattern.replace(/\*\*/g, '.*').replace(/\*/g, '[^/]*');
|
|
148
|
+
const regex = new RegExp(globPattern.replace(/\//g, '\\/'));
|
|
149
|
+
return regex.test(filePath);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
isSchemaValidationLibrary(source, language) {
|
|
154
|
+
const libraries = this.schemaDetectors[language] || [];
|
|
155
|
+
return libraries.some(lib => source.includes(lib));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
isEnvAccess(node, language) {
|
|
159
|
+
if (language === 'typescript') {
|
|
160
|
+
// process.env.VARIABLE
|
|
161
|
+
return (
|
|
162
|
+
t.isMemberExpression(node) &&
|
|
163
|
+
t.isMemberExpression(node.object) &&
|
|
164
|
+
t.isIdentifier(node.object.object, { name: 'process' }) &&
|
|
165
|
+
t.isIdentifier(node.object.property, { name: 'env' })
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
getEnvProperty(node) {
|
|
172
|
+
if (t.isIdentifier(node.property)) {
|
|
173
|
+
return node.property.name;
|
|
174
|
+
}
|
|
175
|
+
if (t.isStringLiteral(node.property)) {
|
|
176
|
+
return node.property.value;
|
|
177
|
+
}
|
|
178
|
+
return 'unknown';
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
hasDefaultValue(parent) {
|
|
182
|
+
return (
|
|
183
|
+
t.isLogicalExpression(parent) ||
|
|
184
|
+
t.isConditionalExpression(parent) ||
|
|
185
|
+
(t.isAssignmentExpression(parent) && parent.right)
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
isFailFastCall(node, language) {
|
|
190
|
+
if (language === 'typescript') {
|
|
191
|
+
// process.exit(1)
|
|
192
|
+
if (
|
|
193
|
+
t.isMemberExpression(node.callee) &&
|
|
194
|
+
t.isIdentifier(node.callee.object, { name: 'process' }) &&
|
|
195
|
+
t.isIdentifier(node.callee.property, { name: 'exit' })
|
|
196
|
+
) {
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
getFailFastType(node) {
|
|
204
|
+
if (t.isMemberExpression(node.callee)) {
|
|
205
|
+
if (t.isIdentifier(node.callee.property, { name: 'exit' })) {
|
|
206
|
+
return 'process.exit';
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (t.isThrowStatement(node)) {
|
|
210
|
+
return 'throw';
|
|
211
|
+
}
|
|
212
|
+
return 'unknown';
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
isConnectivityCheck(node) {
|
|
216
|
+
if (!t.isCallExpression(node)) return false;
|
|
217
|
+
|
|
218
|
+
// Check for common connectivity patterns
|
|
219
|
+
const patterns = [
|
|
220
|
+
'ping', 'connect', 'healthCheck', 'testConnection',
|
|
221
|
+
'validateConnection', 'checkHealth', 'authenticate'
|
|
222
|
+
];
|
|
223
|
+
|
|
224
|
+
if (t.isMemberExpression(node.callee)) {
|
|
225
|
+
const methodName = node.callee.property?.name;
|
|
226
|
+
return patterns.includes(methodName);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (t.isIdentifier(node.callee)) {
|
|
230
|
+
return patterns.includes(node.callee.name);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
getConnectivityMethod(node) {
|
|
237
|
+
if (t.isMemberExpression(node.callee)) {
|
|
238
|
+
return node.callee.property?.name || 'unknown';
|
|
239
|
+
}
|
|
240
|
+
if (t.isIdentifier(node.callee)) {
|
|
241
|
+
return node.callee.name;
|
|
242
|
+
}
|
|
243
|
+
return 'unknown';
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
isDangerousDefault(node) {
|
|
247
|
+
if (!t.isLogicalExpression(node, { operator: '||' })) {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const rightValue = this.getDefaultValue(node.right);
|
|
252
|
+
const dangerousPatterns = [
|
|
253
|
+
"''", '""', '0', 'null', 'undefined',
|
|
254
|
+
"'localhost'", "'http://localhost'", "'dev'", "'development'"
|
|
255
|
+
];
|
|
256
|
+
|
|
257
|
+
return dangerousPatterns.includes(rightValue);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
isDangerousConditionalDefault(node) {
|
|
261
|
+
if (!t.isConditionalExpression(node)) return false;
|
|
262
|
+
|
|
263
|
+
const consequent = this.getDefaultValue(node.consequent);
|
|
264
|
+
const alternate = this.getDefaultValue(node.alternate);
|
|
265
|
+
|
|
266
|
+
const dangerousPatterns = [
|
|
267
|
+
"''", '""', '0', 'null', 'undefined',
|
|
268
|
+
"'localhost'", "'http://localhost'", "'dev'", "'development'"
|
|
269
|
+
];
|
|
270
|
+
|
|
271
|
+
return dangerousPatterns.includes(consequent) || dangerousPatterns.includes(alternate);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
getDangerousDefaultPattern(node) {
|
|
275
|
+
if (t.isLogicalExpression(node)) {
|
|
276
|
+
return this.getDefaultValue(node.right);
|
|
277
|
+
}
|
|
278
|
+
if (t.isConditionalExpression(node)) {
|
|
279
|
+
const consequent = this.getDefaultValue(node.consequent);
|
|
280
|
+
const alternate = this.getDefaultValue(node.alternate);
|
|
281
|
+
return `${consequent} ? ${alternate}`;
|
|
282
|
+
}
|
|
283
|
+
return 'unknown';
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
getDefaultValue(node) {
|
|
287
|
+
if (t.isStringLiteral(node)) {
|
|
288
|
+
return `'${node.value}'`;
|
|
289
|
+
}
|
|
290
|
+
if (t.isNumericLiteral(node)) {
|
|
291
|
+
return node.value.toString();
|
|
292
|
+
}
|
|
293
|
+
if (t.isNullLiteral(node)) {
|
|
294
|
+
return 'null';
|
|
295
|
+
}
|
|
296
|
+
if (t.isIdentifier(node, { name: 'undefined' })) {
|
|
297
|
+
return 'undefined';
|
|
298
|
+
}
|
|
299
|
+
return 'unknown';
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
isConfigValidation(node, envAccess) {
|
|
303
|
+
// Simple heuristic: if an if statement contains a check for environment variables
|
|
304
|
+
// and has a throw or process.exit in the consequent, it's likely validation
|
|
305
|
+
if (t.isIfStatement(node)) {
|
|
306
|
+
const test = node.test;
|
|
307
|
+
const consequent = node.consequent;
|
|
308
|
+
|
|
309
|
+
// Check if test involves environment variables
|
|
310
|
+
const involvesEnv = this.containsEnvAccess(test);
|
|
311
|
+
|
|
312
|
+
// Check if consequent has fail-fast behavior
|
|
313
|
+
const hasFailFast = this.containsFailFast(consequent);
|
|
314
|
+
|
|
315
|
+
return involvesEnv && hasFailFast;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
containsEnvAccess(node) {
|
|
322
|
+
// Simple traversal to check if node contains process.env access
|
|
323
|
+
let hasEnvAccess = false;
|
|
324
|
+
|
|
325
|
+
traverse(node, {
|
|
326
|
+
MemberExpression: (path) => {
|
|
327
|
+
if (this.isEnvAccess(path.node, 'typescript')) {
|
|
328
|
+
hasEnvAccess = true;
|
|
329
|
+
path.stop();
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}, null, {});
|
|
333
|
+
|
|
334
|
+
return hasEnvAccess;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
containsFailFast(node) {
|
|
338
|
+
// Check if node contains fail-fast patterns
|
|
339
|
+
let hasFailFast = false;
|
|
340
|
+
|
|
341
|
+
traverse(node, {
|
|
342
|
+
CallExpression: (path) => {
|
|
343
|
+
if (this.isFailFastCall(path.node, 'typescript')) {
|
|
344
|
+
hasFailFast = true;
|
|
345
|
+
path.stop();
|
|
346
|
+
}
|
|
347
|
+
},
|
|
348
|
+
ThrowStatement: () => {
|
|
349
|
+
hasFailFast = true;
|
|
350
|
+
}
|
|
351
|
+
}, null, {});
|
|
352
|
+
|
|
353
|
+
return hasFailFast;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
isConfigRelatedError(node) {
|
|
357
|
+
// Check if the error message is configuration-related
|
|
358
|
+
if (t.isThrowStatement(node) && t.isNewExpression(node.argument)) {
|
|
359
|
+
const args = node.argument.arguments;
|
|
360
|
+
if (args.length > 0 && t.isStringLiteral(args[0])) {
|
|
361
|
+
const message = args[0].value.toLowerCase();
|
|
362
|
+
const configKeywords = ['config', 'environment', 'env', 'missing', 'required', 'invalid'];
|
|
363
|
+
return configKeywords.some(keyword => message.includes(keyword));
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
module.exports = C073SymbolBasedAnalyzer;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# S037 - Configure Comprehensive Cache Headers to Prevent Sensitive Data Leakage
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Rule S037 ensures that sensitive HTTP responses explicitly disable caching using a complete set of anti-caching headers:
|
|
6
|
+
|
|
7
|
+
- `Cache-Control: no-store, no-cache, must-revalidate`
|
|
8
|
+
- `Pragma: no-cache`
|
|
9
|
+
- `Expires: 0`
|
|
10
|
+
|
|
11
|
+
These prevent browsers and intermediate proxies from persisting sensitive content (e.g., authenticated pages, profile data, tokens) in cache storage where it could later be exposed.
|
|
12
|
+
|
|
13
|
+
## Framework Support
|
|
14
|
+
|
|
15
|
+
This rule supports multiple web frameworks:
|
|
16
|
+
|
|
17
|
+
- **Express.js**: Route handlers using `app.get()`, `router.post()`, etc.
|
|
18
|
+
- **Next.js**: API routes (`export default function handler`) and App Router (`export async function GET`)
|
|
19
|
+
- **NestJS**: Controller methods with decorators (`@Get()`, `@Post()`, etc.)
|
|
20
|
+
- **Nuxt.js**: Server routes (`export default defineEventHandler`)
|
|
21
|
+
|
|
22
|
+
## Security Impact
|
|
23
|
+
|
|
24
|
+
Without proper cache headers, authenticated or confidential data may remain in browser cache or be served to other users (in shared environments, kiosk systems, or via back/forward navigation). Attackers may retrieve cached responses, exposing session data or personal information.
|
|
25
|
+
|
|
26
|
+
## Rule Details
|
|
27
|
+
|
|
28
|
+
### Required Header Set
|
|
29
|
+
|
|
30
|
+
All of the following must be present for sensitive responses:
|
|
31
|
+
|
|
32
|
+
| Header | Required Value / Directives |
|
|
33
|
+
| ------------- | --------------------------------------------------------------------------- |
|
|
34
|
+
| Cache-Control | `no-store, no-cache, must-revalidate` (order flexible, directives required) |
|
|
35
|
+
| Pragma | `no-cache` |
|
|
36
|
+
| Expires | `0` (or a past date) |
|
|
37
|
+
|
|
38
|
+
### Sensitive Response Indicators (heuristic)
|
|
39
|
+
|
|
40
|
+
The rule assumes a response is sensitive when code references identifiers like: `session`, `auth`, `token`, `jwt`, `csrf`, `user`, `profile`, `payment`, `account`.
|
|
41
|
+
|
|
42
|
+
### Detected Patterns
|
|
43
|
+
|
|
44
|
+
✅ **Compliant Examples:**
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
|
|
48
|
+
res.setHeader("Pragma", "no-cache");
|
|
49
|
+
res.setHeader("Expires", "0");
|
|
50
|
+
res.json({ user: profile });
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
response.set({
|
|
55
|
+
"Cache-Control": "no-store, no-cache, must-revalidate",
|
|
56
|
+
Pragma: "no-cache",
|
|
57
|
+
Expires: "0",
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
❌ **Violation Examples:**
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
// Missing Cache-Control directives
|
|
65
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
66
|
+
res.setHeader("Pragma", "no-cache");
|
|
67
|
+
res.setHeader("Expires", "0");
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
// Missing Pragma and Expires
|
|
72
|
+
res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
// Missing all anti-cache headers
|
|
77
|
+
res.json({ auth: token, profile });
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Partial Cache-Control Values
|
|
81
|
+
|
|
82
|
+
The rule flags missing directives individually when `Cache-Control` is present but incomplete.
|
|
83
|
+
|
|
84
|
+
| Example Value | Result |
|
|
85
|
+
| ------------------------------------- | ----------------------------------------- |
|
|
86
|
+
| `no-cache` | ❌ Missing: `no-store`, `must-revalidate` |
|
|
87
|
+
| `no-store, no-cache` | ❌ Missing: `must-revalidate` |
|
|
88
|
+
| `no-store, no-cache, must-revalidate` | ✅ Valid |
|
|
89
|
+
|
|
90
|
+
## Analysis Approach
|
|
91
|
+
|
|
92
|
+
### Symbol-Based (Primary)
|
|
93
|
+
|
|
94
|
+
Parses AST to collect header setting calls across a file, then evaluates completeness.
|
|
95
|
+
|
|
96
|
+
### Regex-Based (Fallback)
|
|
97
|
+
|
|
98
|
+
Scans for literal `setHeader`/`set` calls to approximate detection when semantic engine unavailable.
|
|
99
|
+
|
|
100
|
+
## Configuration
|
|
101
|
+
|
|
102
|
+
See `config.json` for adjustable keys:
|
|
103
|
+
|
|
104
|
+
- `validation.required` – mandatory headers & directives
|
|
105
|
+
- `validation.sensitiveIndicators` – triggers stricter enforcement when detected
|
|
106
|
+
- `patterns.include/exclude` – file targeting
|
|
107
|
+
|
|
108
|
+
## Best Practices
|
|
109
|
+
|
|
110
|
+
1. Always send complete anti-cache headers for authenticated pages.
|
|
111
|
+
2. Never rely only on `Pragma` or only `Cache-Control`.
|
|
112
|
+
3. Include `no-store` for maximum protection (prevents any storage).
|
|
113
|
+
4. Pair with other security headers (CSP, X-Content-Type-Options, etc.).
|
|
114
|
+
5. Re-check reverse proxy / CDN behavior (may override headers).
|
|
115
|
+
|
|
116
|
+
## Related Rules
|
|
117
|
+
|
|
118
|
+
- **S031**: Secure flag for cookies
|
|
119
|
+
- **S032**: HttpOnly flag for cookies
|
|
120
|
+
- **S033**: SameSite attribute
|
|
121
|
+
- **S034**: \_\_Host- prefix
|
|
122
|
+
- **S035**: Path attribute
|
|
123
|
+
|
|
124
|
+
## References
|
|
125
|
+
|
|
126
|
+
- MDN: HTTP Caching
|
|
127
|
+
- OWASP: Sensitive Data Exposure / Cryptographic Failures
|
|
128
|
+
- RFC 7234 - HTTP/1.1 Caching
|