@sun-asterisk/sunlint 1.3.0 → 1.3.2
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 +115 -1
- package/CONTRIBUTING.md +249 -605
- package/README.md +3 -4
- package/config/ci-cd.json +54 -0
- package/config/development.json +56 -0
- package/config/large-project.json +143 -0
- package/config/presets/all.json +0 -1
- package/config/release.json +70 -0
- package/config/rule-analysis-strategies.js +38 -3
- package/config/rules/enhanced-rules-registry.json +474 -1179
- package/config/rules/rules-registry-generated.json +3 -3
- package/core/cli-action-handler.js +24 -30
- package/core/cli-program.js +11 -3
- package/core/config-merger.js +29 -2
- package/core/enhanced-rules-registry.js +3 -2
- package/core/semantic-engine.js +129 -19
- package/core/semantic-rule-base.js +4 -2
- package/core/unified-rule-registry.js +1 -1
- package/docs/COMMAND-EXAMPLES.md +134 -0
- package/docs/LARGE-PROJECT-GUIDE.md +324 -0
- package/engines/heuristic-engine.js +135 -16
- package/integrations/eslint/plugin/index.js +0 -2
- package/integrations/eslint/plugin/rules/common/c003-no-vague-abbreviations.js +59 -1
- package/integrations/eslint/plugin/rules/common/c006-function-name-verb-noun.js +26 -1
- package/integrations/eslint/plugin/rules/common/c030-use-custom-error-classes.js +54 -19
- package/origin-rules/common-en.md +19 -15
- package/package.json +1 -1
- package/rules/common/C002_no_duplicate_code/analyzer.js +334 -36
- package/rules/common/C003_no_vague_abbreviations/analyzer.js +220 -35
- package/rules/common/C006_function_naming/analyzer.js +29 -3
- package/rules/common/C010_limit_block_nesting/analyzer.js +181 -337
- package/rules/common/C010_limit_block_nesting/config.json +64 -0
- package/rules/common/C010_limit_block_nesting/regex-based-analyzer.js +379 -0
- package/rules/common/C010_limit_block_nesting/symbol-based-analyzer.js +231 -0
- package/rules/common/C013_no_dead_code/analyzer.js +75 -177
- package/rules/common/C013_no_dead_code/config.json +61 -0
- package/rules/common/C013_no_dead_code/regex-based-analyzer.js +345 -0
- package/rules/common/C013_no_dead_code/symbol-based-analyzer.js +640 -0
- package/rules/common/C014_dependency_injection/analyzer.js +48 -313
- package/rules/common/C014_dependency_injection/config.json +26 -0
- package/rules/common/C014_dependency_injection/symbol-based-analyzer.js +751 -0
- package/rules/common/C017_constructor_logic/analyzer.js +254 -17
- package/rules/common/C017_constructor_logic/semantic-analyzer.js +340 -0
- package/rules/common/C018_no_throw_generic_error/analyzer.js +232 -0
- package/rules/common/C018_no_throw_generic_error/config.json +50 -0
- package/rules/common/C018_no_throw_generic_error/regex-based-analyzer.js +387 -0
- package/rules/common/C018_no_throw_generic_error/symbol-based-analyzer.js +314 -0
- package/rules/common/C019_log_level_usage/analyzer.js +110 -317
- package/rules/common/C019_log_level_usage/pattern-analyzer.js +88 -0
- package/rules/common/C019_log_level_usage/system-log-analyzer.js +1267 -0
- package/rules/common/C023_no_duplicate_variable/analyzer.js +180 -0
- package/rules/common/C023_no_duplicate_variable/config.json +50 -0
- package/rules/common/C023_no_duplicate_variable/symbol-based-analyzer.js +158 -0
- package/rules/common/C024_no_scatter_hardcoded_constants/analyzer.js +180 -0
- package/rules/common/C024_no_scatter_hardcoded_constants/config.json +50 -0
- package/rules/common/C024_no_scatter_hardcoded_constants/symbol-based-analyzer.js +181 -0
- package/rules/common/C030_use_custom_error_classes/analyzer.js +200 -0
- package/rules/common/C033_separate_service_repository/README.md +78 -0
- package/rules/common/C033_separate_service_repository/analyzer.js +160 -0
- package/rules/common/C033_separate_service_repository/config.json +50 -0
- package/rules/common/C033_separate_service_repository/regex-based-analyzer.js +585 -0
- package/rules/common/C033_separate_service_repository/symbol-based-analyzer.js +368 -0
- package/rules/common/C035_error_logging_context/STRATEGY.md +99 -0
- package/rules/common/C035_error_logging_context/analyzer.js +232 -0
- package/rules/common/C035_error_logging_context/config.json +54 -0
- package/rules/common/C035_error_logging_context/regex-based-analyzer.js +299 -0
- package/rules/common/C035_error_logging_context/symbol-based-analyzer.js +454 -0
- package/rules/common/C040_centralized_validation/analyzer.js +165 -0
- package/rules/common/C040_centralized_validation/config.json +46 -0
- package/rules/common/C040_centralized_validation/regex-based-analyzer.js +243 -0
- package/rules/common/C040_centralized_validation/symbol-based-analyzer.js +416 -0
- package/rules/common/{C076_single_test_behavior → C072_single_test_behavior}/analyzer.js +6 -6
- package/rules/common/C076_explicit_function_types/README.md +30 -0
- package/rules/common/C076_explicit_function_types/analyzer.js +172 -0
- package/rules/common/C076_explicit_function_types/config.json +15 -0
- package/rules/common/C076_explicit_function_types/semantic-analyzer.js +341 -0
- package/rules/index.js +6 -1
- package/rules/parser/rule-parser.js +13 -2
- package/rules/security/S005_no_origin_auth/README.md +226 -0
- package/rules/security/S005_no_origin_auth/analyzer.js +184 -0
- package/rules/security/S005_no_origin_auth/ast-analyzer.js +406 -0
- package/rules/security/S005_no_origin_auth/config.json +85 -0
- package/rules/security/S006_no_plaintext_recovery_codes/README.md +139 -0
- package/rules/security/S006_no_plaintext_recovery_codes/analyzer.js +306 -0
- package/rules/security/S006_no_plaintext_recovery_codes/config.json +48 -0
- package/rules/security/S007_no_plaintext_otp/README.md +198 -0
- package/rules/security/S007_no_plaintext_otp/analyzer.js +406 -0
- package/rules/security/S007_no_plaintext_otp/config.json +79 -0
- package/rules/security/S007_no_plaintext_otp/semantic-analyzer.js +609 -0
- package/rules/security/S007_no_plaintext_otp/semantic-config.json +195 -0
- package/rules/security/S007_no_plaintext_otp/semantic-wrapper.js +280 -0
- package/rules/security/S009_no_insecure_encryption/README.md +158 -0
- package/rules/security/S009_no_insecure_encryption/analyzer.js +319 -0
- package/rules/security/S009_no_insecure_encryption/config.json +55 -0
- package/rules/security/S010_no_insecure_encryption/README.md +224 -0
- package/rules/security/S010_no_insecure_encryption/analyzer.js +493 -0
- package/rules/security/S010_no_insecure_encryption/config.json +48 -0
- package/rules/security/S016_no_sensitive_querystring/STRATEGY.md +149 -0
- package/rules/security/S016_no_sensitive_querystring/analyzer.js +276 -0
- package/rules/security/S016_no_sensitive_querystring/config.json +127 -0
- package/rules/security/S016_no_sensitive_querystring/regex-based-analyzer.js +258 -0
- package/rules/security/S016_no_sensitive_querystring/symbol-based-analyzer.js +495 -0
- package/rules/security/S027_no_hardcoded_secrets/analyzer.js +180 -366
- package/rules/security/S027_no_hardcoded_secrets/categories.json +153 -0
- package/rules/security/S027_no_hardcoded_secrets/categorized-analyzer.js +250 -0
- package/rules/security/S048_no_current_password_in_reset/README.md +222 -0
- package/rules/security/S048_no_current_password_in_reset/analyzer.js +366 -0
- package/rules/security/S048_no_current_password_in_reset/config.json +48 -0
- package/rules/security/S055_content_type_validation/README.md +176 -0
- package/rules/security/S055_content_type_validation/analyzer.js +312 -0
- package/rules/security/S055_content_type_validation/config.json +48 -0
- package/rules/utils/rule-helpers.js +140 -1
- package/scripts/consolidate-config.js +116 -0
- package/scripts/prepare-release.sh +1 -1
- package/config/rules/rules-registry.json +0 -765
- package/docs/ESLINT-INTEGRATION-STRATEGY.md +0 -392
- package/docs/FUTURE_PACKAGES.md +0 -83
- package/docs/HEURISTIC_VS_AI.md +0 -113
- package/docs/PRODUCTION_DEPLOYMENT_ANALYSIS.md +0 -112
- package/docs/PRODUCTION_SIZE_IMPACT.md +0 -183
- package/docs/RELEASE_GUIDE.md +0 -230
- package/docs/STANDARDIZED-CATEGORY-FILTERING.md +0 -156
- package/integrations/eslint/plugin/rules/common/c076-single-behavior-per-test.js +0 -254
- package/rules/common/C006_function_naming/smart-analyzer.js +0 -503
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Heuristic analyzer for S055 - Content-Type Validation in REST Services
|
|
3
|
+
* Purpose: Detect REST endpoints that process request body without validating Content-Type
|
|
4
|
+
* Based on OWASP ASVS 13.2.5 - Input Validation
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
class S055Analyzer {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.ruleId = 'S055';
|
|
10
|
+
this.ruleName = 'Content-Type Validation in REST Services';
|
|
11
|
+
this.description = 'Verify that REST services explicitly check the incoming Content-Type';
|
|
12
|
+
|
|
13
|
+
// HTTP methods that typically have request bodies
|
|
14
|
+
this.httpMethodsWithBody = [
|
|
15
|
+
'post', 'put', 'patch', 'delete'
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
// Patterns that indicate request body usage
|
|
19
|
+
this.requestBodyPatterns = [
|
|
20
|
+
// Express.js patterns
|
|
21
|
+
/req\.body/i,
|
|
22
|
+
/request\.body/i,
|
|
23
|
+
|
|
24
|
+
// NestJS patterns
|
|
25
|
+
/@Body\(\)/i,
|
|
26
|
+
/@Body\([^)]*\)/i,
|
|
27
|
+
|
|
28
|
+
// Generic body access patterns
|
|
29
|
+
/\.body\s*[;\.,\]\}]/i,
|
|
30
|
+
/body\s*[:=]/i,
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
// Patterns that indicate Content-Type validation
|
|
34
|
+
this.contentTypeValidationPatterns = [
|
|
35
|
+
// Express.js validation methods
|
|
36
|
+
/req\.is\s*\(\s*['"`][^'"`]*application\/[^'"`]*['"`]\s*\)/i,
|
|
37
|
+
/request\.is\s*\(\s*['"`][^'"`]*application\/[^'"`]*['"`]\s*\)/i,
|
|
38
|
+
|
|
39
|
+
// Direct header checks
|
|
40
|
+
/req\.headers\s*\[\s*['"`]content-type['"`]\s*\]/i,
|
|
41
|
+
/request\.headers\s*\[\s*['"`]content-type['"`]\s*\]/i,
|
|
42
|
+
/req\.get\s*\(\s*['"`]content-type['"`]\s*\)/i,
|
|
43
|
+
/request\.get\s*\(\s*['"`]content-type['"`]\s*\)/i,
|
|
44
|
+
|
|
45
|
+
// Content-Type comparison
|
|
46
|
+
/content-type\s*[=!]==?\s*['"`]application\//i,
|
|
47
|
+
/['"`]application\/[^'"`]*['"`]\s*[=!]==?\s*.*content-type/i,
|
|
48
|
+
|
|
49
|
+
// Middleware patterns
|
|
50
|
+
/express\.json\s*\(/i,
|
|
51
|
+
/bodyParser\.json\s*\(/i,
|
|
52
|
+
/app\.use\s*\([^)]*json[^)]*\)/i,
|
|
53
|
+
|
|
54
|
+
// NestJS decorators
|
|
55
|
+
/@Header\s*\(\s*['"`]Content-Type['"`]/i,
|
|
56
|
+
/@UseInterceptors\s*\([^)]*ContentType[^)]*\)/i,
|
|
57
|
+
|
|
58
|
+
// Custom validation functions
|
|
59
|
+
/validateContentType/i,
|
|
60
|
+
/checkContentType/i,
|
|
61
|
+
/verifyContentType/i,
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
// Patterns that indicate HTTP method handlers
|
|
65
|
+
this.httpHandlerPatterns = [
|
|
66
|
+
// Express.js route definitions
|
|
67
|
+
/app\.(post|put|patch|delete)\s*\(/i,
|
|
68
|
+
/router\.(post|put|patch|delete)\s*\(/i,
|
|
69
|
+
/express\(\)\.(post|put|patch|delete)\s*\(/i,
|
|
70
|
+
|
|
71
|
+
// NestJS decorators
|
|
72
|
+
/@(Post|Put|Patch|Delete)\s*\(/i,
|
|
73
|
+
|
|
74
|
+
// Generic handler patterns
|
|
75
|
+
/(post|put|patch|delete)\s*:\s*(async\s+)?function/i,
|
|
76
|
+
/(post|put|patch|delete)\s*:\s*\(/i,
|
|
77
|
+
|
|
78
|
+
// Function names indicating HTTP handlers
|
|
79
|
+
/function\s+(handle|process)?(Post|Put|Patch|Delete)/i,
|
|
80
|
+
/const\s+\w*(post|put|patch|delete)\w*\s*=/i,
|
|
81
|
+
/let\s+\w*(post|put|patch|delete)\w*\s*=/i,
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
// Safe patterns to exclude from violations
|
|
85
|
+
this.safePatterns = [
|
|
86
|
+
// Comments and documentation
|
|
87
|
+
/\/\/|\/\*|\*\/|@param|@return|@example/,
|
|
88
|
+
|
|
89
|
+
// Import/export statements
|
|
90
|
+
/import|export|require|module\.exports/i,
|
|
91
|
+
|
|
92
|
+
// Type definitions
|
|
93
|
+
/interface|type|enum|declare/i,
|
|
94
|
+
|
|
95
|
+
// Test files patterns
|
|
96
|
+
/describe\s*\(|it\s*\(|test\s*\(|expect\s*\(/i,
|
|
97
|
+
|
|
98
|
+
// Configuration and constants
|
|
99
|
+
/const\s+\w+\s*=\s*['"`]/i,
|
|
100
|
+
|
|
101
|
+
// Logging and debugging
|
|
102
|
+
/console\.|logger\.|log\(/i,
|
|
103
|
+
|
|
104
|
+
// Middleware already handling Content-Type
|
|
105
|
+
/express\.json|bodyParser\.json|multer\(/i,
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
// Patterns indicating secure implementations
|
|
109
|
+
this.secureImplementationPatterns = [
|
|
110
|
+
// Middleware usage that handles Content-Type
|
|
111
|
+
/app\.use\s*\([^)]*express\.json[^)]*\)/i,
|
|
112
|
+
/app\.use\s*\([^)]*bodyParser\.json[^)]*\)/i,
|
|
113
|
+
|
|
114
|
+
// Global Content-Type validation
|
|
115
|
+
/app\.use\s*\([^)]*validateContentType[^)]*\)/i,
|
|
116
|
+
/app\.use\s*\([^)]*checkContentType[^)]*\)/i,
|
|
117
|
+
];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async analyze(files, language, options = {}) {
|
|
121
|
+
const violations = [];
|
|
122
|
+
|
|
123
|
+
for (const filePath of files) {
|
|
124
|
+
// Skip test files, build directories, and node_modules
|
|
125
|
+
if (this.shouldSkipFile(filePath)) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const content = require('fs').readFileSync(filePath, 'utf8');
|
|
131
|
+
const fileViolations = this.analyzeFile(content, filePath, options);
|
|
132
|
+
violations.push(...fileViolations);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
if (options.verbose) {
|
|
135
|
+
console.warn(`⚠️ Failed to analyze ${filePath}: ${error.message}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return violations;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
shouldSkipFile(filePath) {
|
|
144
|
+
const skipPatterns = [
|
|
145
|
+
'test/', 'tests/', '__tests__/', '.test.', '.spec.',
|
|
146
|
+
'node_modules/', 'build/', 'dist/', '.next/', 'coverage/',
|
|
147
|
+
'vendor/', 'mocks/', '.mock.',
|
|
148
|
+
// Config files
|
|
149
|
+
'config/', 'configs/', '.config.',
|
|
150
|
+
// Static assets
|
|
151
|
+
'public/', 'static/', 'assets/',
|
|
152
|
+
];
|
|
153
|
+
|
|
154
|
+
return skipPatterns.some(pattern => filePath.includes(pattern));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
analyzeFile(content, filePath, options = {}) {
|
|
158
|
+
const violations = [];
|
|
159
|
+
const lines = content.split('\n');
|
|
160
|
+
|
|
161
|
+
// First, check if file has global Content-Type validation (middleware)
|
|
162
|
+
const hasGlobalValidation = this.hasGlobalContentTypeValidation(content);
|
|
163
|
+
|
|
164
|
+
lines.forEach((line, index) => {
|
|
165
|
+
const lineNumber = index + 1;
|
|
166
|
+
const trimmedLine = line.trim();
|
|
167
|
+
|
|
168
|
+
// Skip comments, imports, and empty lines
|
|
169
|
+
if (this.shouldSkipLine(trimmedLine)) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Check for potential Content-Type validation violations
|
|
174
|
+
const violation = this.checkForContentTypeViolation(
|
|
175
|
+
line,
|
|
176
|
+
lineNumber,
|
|
177
|
+
filePath,
|
|
178
|
+
content,
|
|
179
|
+
hasGlobalValidation
|
|
180
|
+
);
|
|
181
|
+
if (violation) {
|
|
182
|
+
violations.push(violation);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
return violations;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
shouldSkipLine(line) {
|
|
190
|
+
return (
|
|
191
|
+
line.length === 0 ||
|
|
192
|
+
this.safePatterns.some(pattern => pattern.test(line))
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
hasGlobalContentTypeValidation(content) {
|
|
197
|
+
return this.secureImplementationPatterns.some(pattern => pattern.test(content));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
checkForContentTypeViolation(line, lineNumber, filePath, fullContent, hasGlobalValidation) {
|
|
201
|
+
// Check if line contains request body usage
|
|
202
|
+
const hasRequestBodyUsage = this.requestBodyPatterns.some(pattern => pattern.test(line));
|
|
203
|
+
|
|
204
|
+
if (!hasRequestBodyUsage) {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Check if this line is part of an HTTP handler
|
|
209
|
+
const isInHttpHandler = this.isInHttpHandlerContext(line, lineNumber, fullContent);
|
|
210
|
+
|
|
211
|
+
if (!isInHttpHandler) {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Skip if there's global validation
|
|
216
|
+
if (hasGlobalValidation) {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Check if there's local Content-Type validation in the same function/handler
|
|
221
|
+
const hasLocalValidation = this.hasLocalContentTypeValidation(lineNumber, fullContent);
|
|
222
|
+
|
|
223
|
+
if (hasLocalValidation) {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Check if this is a NestJS handler with proper decorators
|
|
228
|
+
if (this.isSecureNestJSHandler(lineNumber, fullContent)) {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
ruleId: this.ruleId,
|
|
234
|
+
severity: 'error',
|
|
235
|
+
message: 'REST endpoint processes request body without validating Content-Type header. This can lead to security vulnerabilities.',
|
|
236
|
+
line: lineNumber,
|
|
237
|
+
column: this.findPatternColumn(line, this.requestBodyPatterns),
|
|
238
|
+
filePath: filePath,
|
|
239
|
+
type: 'missing_content_type_validation',
|
|
240
|
+
details: 'Consider adding Content-Type validation using req.is("application/json") or checking req.headers["content-type"] before processing request body.'
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
isInHttpHandlerContext(line, lineNumber, fullContent) {
|
|
245
|
+
const lines = fullContent.split('\n');
|
|
246
|
+
|
|
247
|
+
// Check previous lines for HTTP handler patterns
|
|
248
|
+
const contextRange = Math.max(0, lineNumber - 10); // Check up to 10 lines back
|
|
249
|
+
|
|
250
|
+
for (let i = contextRange; i < lineNumber; i++) {
|
|
251
|
+
const contextLine = lines[i];
|
|
252
|
+
if (this.httpHandlerPatterns.some(pattern => pattern.test(contextLine))) {
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Check current line
|
|
258
|
+
if (this.httpHandlerPatterns.some(pattern => pattern.test(line))) {
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
hasLocalContentTypeValidation(lineNumber, fullContent) {
|
|
266
|
+
const lines = fullContent.split('\n');
|
|
267
|
+
|
|
268
|
+
// Check surrounding lines for Content-Type validation
|
|
269
|
+
const startLine = Math.max(0, lineNumber - 15);
|
|
270
|
+
const endLine = Math.min(lines.length, lineNumber + 10);
|
|
271
|
+
|
|
272
|
+
for (let i = startLine; i < endLine; i++) {
|
|
273
|
+
const checkLine = lines[i];
|
|
274
|
+
if (this.contentTypeValidationPatterns.some(pattern => pattern.test(checkLine))) {
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
isSecureNestJSHandler(lineNumber, fullContent) {
|
|
283
|
+
const lines = fullContent.split('\n');
|
|
284
|
+
|
|
285
|
+
// Check previous lines for NestJS decorators that handle Content-Type
|
|
286
|
+
const contextRange = Math.max(0, lineNumber - 5);
|
|
287
|
+
|
|
288
|
+
for (let i = contextRange; i < lineNumber; i++) {
|
|
289
|
+
const line = lines[i];
|
|
290
|
+
if (/@Header\s*\(\s*['"`]Content-Type['"`]/i.test(line)) {
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
if (/@UseInterceptors\s*\([^)]*ContentType[^)]*\)/i.test(line)) {
|
|
294
|
+
return true;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
findPatternColumn(line, patterns) {
|
|
302
|
+
for (const pattern of patterns) {
|
|
303
|
+
const match = pattern.exec(line);
|
|
304
|
+
if (match) {
|
|
305
|
+
return match.index + 1;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return 1;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
module.exports = S055Analyzer;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"ruleId": "S055",
|
|
3
|
+
"name": "Content-Type Validation in REST Services",
|
|
4
|
+
"description": "Verify that REST services explicitly check the incoming Content-Type to be the expected one",
|
|
5
|
+
"category": "security",
|
|
6
|
+
"severity": "error",
|
|
7
|
+
"languages": ["typescript", "javascript"],
|
|
8
|
+
"tags": ["security", "owasp", "rest-api", "input-validation", "asvs"],
|
|
9
|
+
"enabled": true,
|
|
10
|
+
"fixable": false,
|
|
11
|
+
"engine": "heuristic",
|
|
12
|
+
"metadata": {
|
|
13
|
+
"owaspCategory": "OWASP ASVS 13.2.5",
|
|
14
|
+
"cweId": "CWE-20",
|
|
15
|
+
"description": "REST services should validate the Content-Type header to ensure they receive data in the expected format. Missing Content-Type validation can lead to security vulnerabilities where attackers send malicious data in unexpected formats.",
|
|
16
|
+
"impact": "Medium - Data injection, parsing errors, security bypass",
|
|
17
|
+
"likelihood": "Medium",
|
|
18
|
+
"remediation": "Always validate Content-Type header before processing request body in REST endpoints"
|
|
19
|
+
},
|
|
20
|
+
"patterns": {
|
|
21
|
+
"vulnerable": [
|
|
22
|
+
"Processing req.body without checking Content-Type",
|
|
23
|
+
"Accepting any Content-Type in REST endpoints",
|
|
24
|
+
"Missing Content-Type validation in Express/NestJS handlers",
|
|
25
|
+
"Directly parsing request body without format validation"
|
|
26
|
+
],
|
|
27
|
+
"secure": [
|
|
28
|
+
"Using req.is('application/json') to validate Content-Type",
|
|
29
|
+
"Checking req.headers['content-type'] before processing",
|
|
30
|
+
"Rejecting requests with unexpected Content-Type",
|
|
31
|
+
"Using middleware for Content-Type validation"
|
|
32
|
+
]
|
|
33
|
+
},
|
|
34
|
+
"examples": {
|
|
35
|
+
"violations": [
|
|
36
|
+
"app.post('/api/users', (req, res) => { const user = req.body; });",
|
|
37
|
+
"router.put('/data', (req, res) => { processData(req.body); });",
|
|
38
|
+
"async createUser(req, res) { await userService.create(req.body); }",
|
|
39
|
+
"@Post() create(@Body() data: any) { return this.service.create(data); }"
|
|
40
|
+
],
|
|
41
|
+
"fixes": [
|
|
42
|
+
"if (!req.is('application/json')) return res.status(415).send('Unsupported Media Type');",
|
|
43
|
+
"if (req.headers['content-type'] !== 'application/json') throw new Error('Invalid Content-Type');",
|
|
44
|
+
"app.use(express.json({ type: 'application/json' }));",
|
|
45
|
+
"@Post() @Header('Content-Type', 'application/json') create(@Body() data: CreateDto) {}"
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -261,4 +261,143 @@ class RuleHelper {
|
|
|
261
261
|
}
|
|
262
262
|
}
|
|
263
263
|
|
|
264
|
-
|
|
264
|
+
/**
|
|
265
|
+
* Comment Detection Utilities
|
|
266
|
+
* Reusable functions for detecting and handling comments in source code
|
|
267
|
+
*/
|
|
268
|
+
class CommentDetector {
|
|
269
|
+
/**
|
|
270
|
+
* Check if a line is within a block comment region
|
|
271
|
+
* @param {string[]} lines - Array of lines
|
|
272
|
+
* @param {number} lineIndex - Current line index (0-based)
|
|
273
|
+
* @returns {boolean} True if line is in block comment
|
|
274
|
+
*/
|
|
275
|
+
static isLineInBlockComment(lines, lineIndex) {
|
|
276
|
+
let inBlockComment = false;
|
|
277
|
+
|
|
278
|
+
for (let i = 0; i <= lineIndex; i++) {
|
|
279
|
+
const line = lines[i];
|
|
280
|
+
|
|
281
|
+
// Check for block comment start
|
|
282
|
+
if (line.includes('/*')) {
|
|
283
|
+
inBlockComment = true;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Check for block comment end on same line or later lines
|
|
287
|
+
if (line.includes('*/')) {
|
|
288
|
+
inBlockComment = false;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return inBlockComment;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Check if a specific position in a line is inside a comment
|
|
297
|
+
* @param {string} line - Line content
|
|
298
|
+
* @param {number} position - Character position in line
|
|
299
|
+
* @returns {boolean} True if position is inside a comment
|
|
300
|
+
*/
|
|
301
|
+
static isPositionInComment(line, position) {
|
|
302
|
+
// Check if position is after // comment
|
|
303
|
+
const singleLineCommentPos = line.indexOf('//');
|
|
304
|
+
if (singleLineCommentPos !== -1 && position > singleLineCommentPos) {
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Check if position is inside /* */ comment on same line
|
|
309
|
+
let pos = 0;
|
|
310
|
+
while (pos < line.length) {
|
|
311
|
+
const commentStart = line.indexOf('/*', pos);
|
|
312
|
+
const commentEnd = line.indexOf('*/', pos);
|
|
313
|
+
|
|
314
|
+
if (commentStart !== -1 && commentEnd !== -1 && commentStart < commentEnd) {
|
|
315
|
+
if (position >= commentStart && position <= commentEnd + 1) {
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
pos = commentEnd + 2;
|
|
319
|
+
} else {
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Clean line by removing comments but preserving structure for regex matching
|
|
329
|
+
* @param {string} line - Original line
|
|
330
|
+
* @returns {object} { cleanLine, commentRanges }
|
|
331
|
+
*/
|
|
332
|
+
static cleanLineForMatching(line) {
|
|
333
|
+
let cleanLine = line;
|
|
334
|
+
const commentRanges = [];
|
|
335
|
+
|
|
336
|
+
// Track /* */ comments
|
|
337
|
+
let pos = 0;
|
|
338
|
+
while (pos < cleanLine.length) {
|
|
339
|
+
const commentStart = cleanLine.indexOf('/*', pos);
|
|
340
|
+
const commentEnd = cleanLine.indexOf('*/', pos);
|
|
341
|
+
|
|
342
|
+
if (commentStart !== -1 && commentEnd !== -1 && commentStart < commentEnd) {
|
|
343
|
+
commentRanges.push({ start: commentStart, end: commentEnd + 2 });
|
|
344
|
+
// Replace with spaces to preserve positions
|
|
345
|
+
const spaces = ' '.repeat(commentEnd + 2 - commentStart);
|
|
346
|
+
cleanLine = cleanLine.substring(0, commentStart) + spaces + cleanLine.substring(commentEnd + 2);
|
|
347
|
+
pos = commentEnd + 2;
|
|
348
|
+
} else {
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Track // comments
|
|
354
|
+
const singleCommentPos = cleanLine.indexOf('//');
|
|
355
|
+
if (singleCommentPos !== -1) {
|
|
356
|
+
commentRanges.push({ start: singleCommentPos, end: cleanLine.length });
|
|
357
|
+
cleanLine = cleanLine.substring(0, singleCommentPos);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return { cleanLine, commentRanges };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Filter out comment lines from analysis
|
|
365
|
+
* @param {string[]} lines - Array of lines
|
|
366
|
+
* @returns {Array} Array of {line, lineNumber, isComment} objects
|
|
367
|
+
*/
|
|
368
|
+
static filterCommentLines(lines) {
|
|
369
|
+
const result = [];
|
|
370
|
+
let inBlockComment = false;
|
|
371
|
+
|
|
372
|
+
lines.forEach((line, index) => {
|
|
373
|
+
const trimmedLine = line.trim();
|
|
374
|
+
|
|
375
|
+
// Track block comments
|
|
376
|
+
if (trimmedLine.includes('/*')) {
|
|
377
|
+
inBlockComment = true;
|
|
378
|
+
}
|
|
379
|
+
if (trimmedLine.includes('*/')) {
|
|
380
|
+
inBlockComment = false;
|
|
381
|
+
result.push({ line, lineNumber: index + 1, isComment: true });
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
if (inBlockComment) {
|
|
385
|
+
result.push({ line, lineNumber: index + 1, isComment: true });
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Check single line comments
|
|
390
|
+
const isComment = trimmedLine.startsWith('//') || trimmedLine.startsWith('#');
|
|
391
|
+
|
|
392
|
+
result.push({
|
|
393
|
+
line,
|
|
394
|
+
lineNumber: index + 1,
|
|
395
|
+
isComment
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
return result;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
module.exports = { RuleHelper, CommentDetector };
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Script to consolidate all rules from rules-registry.json into enhanced-rules-registry.json
|
|
5
|
+
* then remove the old file to avoid config conflicts
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
const oldRegistryPath = '/Users/bach.ngoc.hoai/Docs/ee/coding-quality/extensions/sunlint/config/rules/rules-registry.json';
|
|
12
|
+
const enhancedRegistryPath = '/Users/bach.ngoc.hoai/Docs/ee/coding-quality/extensions/sunlint/config/rules/enhanced-rules-registry.json';
|
|
13
|
+
|
|
14
|
+
console.log('🔄 Consolidating rule configurations...');
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
// Read both files
|
|
18
|
+
const oldRegistry = JSON.parse(fs.readFileSync(oldRegistryPath, 'utf8'));
|
|
19
|
+
const enhancedRegistry = JSON.parse(fs.readFileSync(enhancedRegistryPath, 'utf8'));
|
|
20
|
+
|
|
21
|
+
console.log(`📊 Old registry has ${Object.keys(oldRegistry.rules).length} rules`);
|
|
22
|
+
console.log(`📊 Enhanced registry has ${Object.keys(enhancedRegistry.rules).length} rules`);
|
|
23
|
+
|
|
24
|
+
// Track what was added
|
|
25
|
+
let addedRules = [];
|
|
26
|
+
let skippedRules = [];
|
|
27
|
+
|
|
28
|
+
// Add rules from old registry that don't exist in enhanced registry
|
|
29
|
+
for (const [ruleId, ruleConfig] of Object.entries(oldRegistry.rules)) {
|
|
30
|
+
if (!enhancedRegistry.rules[ruleId]) {
|
|
31
|
+
console.log(`➕ Adding rule ${ruleId}: ${ruleConfig.name}`);
|
|
32
|
+
enhancedRegistry.rules[ruleId] = ruleConfig;
|
|
33
|
+
addedRules.push(ruleId);
|
|
34
|
+
} else {
|
|
35
|
+
console.log(`⏭️ Skipping rule ${ruleId} (already exists in enhanced registry)`);
|
|
36
|
+
skippedRules.push(ruleId);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Merge categories if needed
|
|
41
|
+
if (oldRegistry.categories) {
|
|
42
|
+
for (const [categoryId, categoryConfig] of Object.entries(oldRegistry.categories)) {
|
|
43
|
+
if (!enhancedRegistry.categories) {
|
|
44
|
+
enhancedRegistry.categories = {};
|
|
45
|
+
}
|
|
46
|
+
if (!enhancedRegistry.categories[categoryId]) {
|
|
47
|
+
console.log(`➕ Adding category ${categoryId}: ${categoryConfig.name}`);
|
|
48
|
+
enhancedRegistry.categories[categoryId] = categoryConfig;
|
|
49
|
+
} else {
|
|
50
|
+
// Merge rules from old category
|
|
51
|
+
const existingRules = new Set(enhancedRegistry.categories[categoryId].rules);
|
|
52
|
+
const newRules = categoryConfig.rules.filter(rule => !existingRules.has(rule));
|
|
53
|
+
if (newRules.length > 0) {
|
|
54
|
+
console.log(`🔄 Merging ${newRules.length} rules into category ${categoryId}`);
|
|
55
|
+
enhancedRegistry.categories[categoryId].rules.push(...newRules);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Merge presets if needed
|
|
62
|
+
if (oldRegistry.presets) {
|
|
63
|
+
for (const [presetId, presetConfig] of Object.entries(oldRegistry.presets)) {
|
|
64
|
+
if (!enhancedRegistry.presets) {
|
|
65
|
+
enhancedRegistry.presets = {};
|
|
66
|
+
}
|
|
67
|
+
if (!enhancedRegistry.presets[presetId]) {
|
|
68
|
+
console.log(`➕ Adding preset ${presetId}: ${presetConfig.name}`);
|
|
69
|
+
enhancedRegistry.presets[presetId] = presetConfig;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Merge languages if needed
|
|
75
|
+
if (oldRegistry.languages) {
|
|
76
|
+
for (const [langId, langConfig] of Object.entries(oldRegistry.languages)) {
|
|
77
|
+
if (!enhancedRegistry.languages) {
|
|
78
|
+
enhancedRegistry.languages = {};
|
|
79
|
+
}
|
|
80
|
+
if (!enhancedRegistry.languages[langId]) {
|
|
81
|
+
console.log(`➕ Adding language ${langId}`);
|
|
82
|
+
enhancedRegistry.languages[langId] = langConfig;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Update metadata
|
|
88
|
+
if (enhancedRegistry.metadata) {
|
|
89
|
+
enhancedRegistry.metadata.totalRules = Object.keys(enhancedRegistry.rules).length;
|
|
90
|
+
enhancedRegistry.metadata.lastUpdated = new Date().toISOString().split('T')[0];
|
|
91
|
+
enhancedRegistry.metadata.consolidatedFrom = oldRegistryPath;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Write enhanced registry back
|
|
95
|
+
fs.writeFileSync(enhancedRegistryPath, JSON.stringify(enhancedRegistry, null, 2));
|
|
96
|
+
|
|
97
|
+
console.log('✅ Consolidation completed!');
|
|
98
|
+
console.log(`📊 Total rules now: ${Object.keys(enhancedRegistry.rules).length}`);
|
|
99
|
+
console.log(`➕ Added rules: ${addedRules.length} - ${addedRules.join(', ')}`);
|
|
100
|
+
console.log(`⏭️ Skipped rules: ${skippedRules.length} - ${skippedRules.join(', ')}`);
|
|
101
|
+
|
|
102
|
+
// Create backup of old registry before deletion
|
|
103
|
+
const backupPath = oldRegistryPath + '.backup';
|
|
104
|
+
fs.copyFileSync(oldRegistryPath, backupPath);
|
|
105
|
+
console.log(`💾 Created backup: ${backupPath}`);
|
|
106
|
+
|
|
107
|
+
// Remove old registry
|
|
108
|
+
fs.unlinkSync(oldRegistryPath);
|
|
109
|
+
console.log(`🗑️ Removed old registry: ${oldRegistryPath}`);
|
|
110
|
+
|
|
111
|
+
console.log('🎉 Configuration consolidation complete!');
|
|
112
|
+
|
|
113
|
+
} catch (error) {
|
|
114
|
+
console.error('❌ Error during consolidation:', error);
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
@@ -135,7 +135,7 @@ sunlint --rule=C006 --input=src --format=summary
|
|
|
135
135
|
|
|
136
136
|
# TypeScript Analysis
|
|
137
137
|
--typescript # Enable TypeScript analysis
|
|
138
|
-
--typescript-engine <type> # Engine: eslint,
|
|
138
|
+
--typescript-engine <type> # Engine: eslint, heuristic, hybrid
|
|
139
139
|
|
|
140
140
|
# Output Control
|
|
141
141
|
--format <format> # Output: eslint, json, summary, table
|