@sun-asterisk/sunlint 1.3.26 → 1.3.28
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/config/rules/enhanced-rules-registry.json +101 -17
- package/config/rules/rules-registry-generated.json +22 -22
- package/origin-rules/security-en.md +351 -338
- package/package.json +1 -1
- package/rules/common/C003_no_vague_abbreviations/analyzer.js +73 -21
- package/rules/common/C017_constructor_logic/symbol-based-analyzer.js +206 -2
- package/rules/common/C024_no_scatter_hardcoded_constants/symbol-based-analyzer.js +553 -58
- package/rules/common/C029_catch_block_logging/analyzer.js +47 -12
- package/rules/common/C033_separate_service_repository/symbol-based-analyzer.js +35 -15
- package/rules/common/C041_no_sensitive_hardcode/symbol-based-analyzer.js +9 -5
- package/rules/security/S003_open_redirect_protection/README.md +371 -0
- package/rules/security/S003_open_redirect_protection/analyzer.js +135 -0
- package/rules/security/S003_open_redirect_protection/config.json +58 -0
- package/rules/security/S003_open_redirect_protection/symbol-based-analyzer.js +884 -0
- package/rules/security/S004_sensitive_data_logging/analyzer.js +135 -0
- package/rules/security/S004_sensitive_data_logging/config.json +62 -0
- package/rules/security/S004_sensitive_data_logging/symbol-based-analyzer.js +592 -0
- package/rules/security/S005_no_origin_auth/analyzer.js +97 -148
- package/rules/security/S005_no_origin_auth/config.json +28 -67
- package/rules/security/S005_no_origin_auth/symbol-based-analyzer.js +708 -0
- package/rules/security/S006_no_plaintext_recovery_codes/symbol-based-analyzer.js +170 -31
- package/rules/security/S010_no_insecure_encryption/analyzer.js +8 -2
- package/rules/security/S012_hardcoded_secrets/analyzer.js +149 -0
- package/rules/security/S012_hardcoded_secrets/config.json +75 -0
- package/rules/security/S012_hardcoded_secrets/symbol-based-analyzer.js +1204 -0
- package/rules/security/S013_tls_enforcement/symbol-based-analyzer.js +87 -0
- package/rules/security/S017_use_parameterized_queries/analyzer.js +11 -78
- package/rules/security/S017_use_parameterized_queries/symbol-based-analyzer.js +1146 -1
- package/rules/security/S019_smtp_injection_protection/analyzer.js +120 -0
- package/rules/security/S019_smtp_injection_protection/config.json +35 -0
- package/rules/security/S019_smtp_injection_protection/symbol-based-analyzer.js +687 -0
- package/rules/security/S020_no_eval_dynamic_code/analyzer.js +55 -130
- package/rules/security/S020_no_eval_dynamic_code/symbol-based-analyzer.js +4 -19
- package/rules/security/S022_escape_output_context/README.md +254 -0
- package/rules/security/S022_escape_output_context/analyzer.js +510 -0
- package/rules/security/S022_escape_output_context/config.json +229 -0
- package/rules/security/S023_no_json_injection/analyzer.js +15 -0
- package/rules/security/S023_no_json_injection/ast-analyzer.js +18 -3
- package/rules/security/S023_no_json_injection/config.json +133 -0
- package/rules/security/S024_xpath_xxe_protection/regex-based-analyzer.js +41 -0
- package/rules/security/S027_no_hardcoded_secrets/analyzer.js +67 -8
- package/rules/security/S027_no_hardcoded_secrets/categorized-analyzer.js +29 -6
- package/rules/security/S029_csrf_protection/config.json +127 -0
- package/rules/security/S030_directory_browsing_protection/regex-based-analyzer.js +160 -28
- package/rules/security/S030_directory_browsing_protection/symbol-based-analyzer.js +81 -19
- package/rules/security/S031_secure_session_cookies/analyzer.js +20 -2
- package/rules/security/S031_secure_session_cookies/regex-based-analyzer.js +100 -0
- package/rules/security/S031_secure_session_cookies/symbol-based-analyzer.js +8 -1
- package/rules/security/S032_httponly_session_cookies/analyzer.js +2 -2
- package/rules/security/S032_httponly_session_cookies/regex-based-analyzer.js +115 -0
- package/rules/security/S032_httponly_session_cookies/symbol-based-analyzer.js +39 -10
- package/rules/security/S036_lfi_rfi_protection/analyzer.js +224 -0
- package/rules/security/S036_lfi_rfi_protection/config.json +20 -0
- package/rules/security/S040_session_fixation_protection/analyzer.js +153 -0
- package/rules/security/S040_session_fixation_protection/config.json +20 -0
- package/rules/security/S042_require_re_authentication_for_long_lived/README.md +83 -0
- package/rules/security/S042_require_re_authentication_for_long_lived/analyzer.js +153 -0
- package/rules/security/S042_require_re_authentication_for_long_lived/config.json +41 -0
- package/rules/security/S042_require_re_authentication_for_long_lived/symbol-based-analyzer.js +1139 -0
- package/rules/security/S043_password_changes_invalidate_all_sessions/README.md +107 -0
- package/rules/security/S043_password_changes_invalidate_all_sessions/analyzer.js +153 -0
- package/rules/security/S043_password_changes_invalidate_all_sessions/config.json +41 -0
- package/rules/security/S043_password_changes_invalidate_all_sessions/symbol-based-analyzer.js +541 -0
- package/docs/COMMAND-EXAMPLES.md +0 -390
- package/docs/FILE_LIMITS_COMPLETION_REPORT.md +0 -151
- package/docs/FOLDER_STRUCTURE.md +0 -59
- package/docs/SIMPLIFIED_USAGE_GUIDE.md +0 -208
- package/rules/security/S017_use_parameterized_queries/regex-based-analyzer.js +0 -541
- package/rules/security/S020_no_eval_dynamic_code/regex-based-analyzer.js +0 -307
|
@@ -29,11 +29,38 @@ class S013SymbolBasedAnalyzer {
|
|
|
29
29
|
try {
|
|
30
30
|
const { SyntaxKind } = require("ts-morph");
|
|
31
31
|
|
|
32
|
+
// Skip test files - they use http:// for mocking
|
|
33
|
+
if (
|
|
34
|
+
filePath.includes('.test.') ||
|
|
35
|
+
filePath.includes('.spec.') ||
|
|
36
|
+
filePath.includes('__tests__/') ||
|
|
37
|
+
filePath.includes('__mocks__/')
|
|
38
|
+
) {
|
|
39
|
+
return violations; // Skip test files
|
|
40
|
+
}
|
|
41
|
+
|
|
32
42
|
// Check for imports/requires of 'http' module
|
|
33
43
|
const importDecls = sourceFile.getImportDeclarations();
|
|
34
44
|
for (const imp of importDecls) {
|
|
35
45
|
const moduleName = imp.getModuleSpecifierValue();
|
|
36
46
|
if (moduleName === "http") {
|
|
47
|
+
// Check if it's only used for typing (e.g., http.IncomingMessage)
|
|
48
|
+
const namedImports = imp.getNamedImports();
|
|
49
|
+
const namespaceImport = imp.getNamespaceImport();
|
|
50
|
+
|
|
51
|
+
// Skip if it's a namespace import used only for types
|
|
52
|
+
if (namespaceImport) {
|
|
53
|
+
const importName = namespaceImport.getText();
|
|
54
|
+
const sourceText = sourceFile.getFullText();
|
|
55
|
+
// Check if only used for typing (e.g., import * as http for http.IncomingMessage type)
|
|
56
|
+
const usagePattern = new RegExp(`${importName}\\.(?:IncomingMessage|ServerResponse|Server)`, 'g');
|
|
57
|
+
const actualUsagePattern = new RegExp(`${importName}\\.(?:createServer|get|request)\\s*\\(`, 'g');
|
|
58
|
+
|
|
59
|
+
if (usagePattern.test(sourceText) && !actualUsagePattern.test(sourceText)) {
|
|
60
|
+
continue; // Only used for typing, not actual HTTP calls
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
37
64
|
const startLine = imp.getStartLineNumber();
|
|
38
65
|
violations.push({
|
|
39
66
|
ruleId: this.ruleId,
|
|
@@ -148,6 +175,30 @@ class S013SymbolBasedAnalyzer {
|
|
|
148
175
|
if (kind === SyntaxKind.StringLiteral) {
|
|
149
176
|
const lit = a.getLiteralValue();
|
|
150
177
|
if (/^http:\/\//i.test(lit)) {
|
|
178
|
+
// Skip if it's a string check in utility functions (e.g., startsWith('http://'))
|
|
179
|
+
if (lit === 'http://' || lit === 'https://') {
|
|
180
|
+
const callText = call.getParent().getText();
|
|
181
|
+
// Check if it's used in string manipulation (startsWith, replace, includes, etc.)
|
|
182
|
+
if (
|
|
183
|
+
/\.(?:startsWith|replace|includes|indexOf|match|test)\s*\(/.test(callText) ||
|
|
184
|
+
/case\s+.*startsWith/.test(callText)
|
|
185
|
+
) {
|
|
186
|
+
continue; // Skip utility functions that convert http -> https
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Skip localhost URLs used as base URL for parsing (e.g., new URL(path, 'http://localhost'))
|
|
191
|
+
if (lit === 'http://localhost' && args.length >= 2) {
|
|
192
|
+
// This is likely used as base URL for URL constructor
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Skip Swagger .addServer() with localhost in development
|
|
197
|
+
const callExprText = expr.getText();
|
|
198
|
+
if (/\.addServer/.test(callExprText) && /localhost/.test(lit)) {
|
|
199
|
+
continue; // Swagger config for local development
|
|
200
|
+
}
|
|
201
|
+
|
|
151
202
|
const msgPrefix = isNewExpression
|
|
152
203
|
? "Constructor"
|
|
153
204
|
: "Insecure client call";
|
|
@@ -176,6 +227,16 @@ class S013SymbolBasedAnalyzer {
|
|
|
176
227
|
) {
|
|
177
228
|
const text = a.getText();
|
|
178
229
|
if (/http:\/\//i.test(text)) {
|
|
230
|
+
// Skip Firebase Emulator configuration (local development only)
|
|
231
|
+
const callExprText = expr.getText();
|
|
232
|
+
if (
|
|
233
|
+
/connect.*Emulator/i.test(callExprText) ||
|
|
234
|
+
(text.includes('localhost') || text.includes('EMULATOR_HOST') || text.includes('127.0.0.1'))
|
|
235
|
+
) {
|
|
236
|
+
// Firebase/local emulator config - safe for development
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
|
|
179
240
|
violations.push({
|
|
180
241
|
ruleId: this.ruleId,
|
|
181
242
|
message: `Insecure URL in template literal detected - use 'https://'`,
|
|
@@ -220,6 +281,11 @@ class S013SymbolBasedAnalyzer {
|
|
|
220
281
|
column: 1,
|
|
221
282
|
});
|
|
222
283
|
} else if (/^http:\/\//i.test(lit)) {
|
|
284
|
+
// Skip localhost URLs used as base URL for URL constructor (e.g., new URL(path, 'http://localhost'))
|
|
285
|
+
if (lit === 'http://localhost' && args.length >= 2) {
|
|
286
|
+
continue; // Used as base URL for parsing
|
|
287
|
+
}
|
|
288
|
+
|
|
223
289
|
violations.push({
|
|
224
290
|
ruleId: this.ruleId,
|
|
225
291
|
message: `Constructor with insecure URL '${lit}' detected - use 'https://'`,
|
|
@@ -266,6 +332,27 @@ class S013SymbolBasedAnalyzer {
|
|
|
266
332
|
if (kind === SyntaxKind.StringLiteral) {
|
|
267
333
|
const value = initializer.getLiteralValue();
|
|
268
334
|
if (/^http:\/\//i.test(value)) {
|
|
335
|
+
// Skip API documentation examples
|
|
336
|
+
if (value === 'http://example.com' || value === 'http://localhost') {
|
|
337
|
+
// Check if this is inside @ApiProperty decorator
|
|
338
|
+
const propertyName = prop.getName && prop.getName();
|
|
339
|
+
const parent = prop.getParent();
|
|
340
|
+
const grandParent = parent && parent.getParent();
|
|
341
|
+
|
|
342
|
+
// Check for @ApiProperty({ example: 'http://...' })
|
|
343
|
+
if (grandParent) {
|
|
344
|
+
const decorators = grandParent.getDecorators && grandParent.getDecorators();
|
|
345
|
+
if (decorators && decorators.some(d => d.getText().includes('ApiProperty'))) {
|
|
346
|
+
continue; // Skip API documentation examples
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Also check if property name is 'example'
|
|
351
|
+
if (propertyName === 'example') {
|
|
352
|
+
continue; // Skip example values
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
269
356
|
violations.push({
|
|
270
357
|
ruleId: this.ruleId,
|
|
271
358
|
message: `Object property contains insecure URL '${value}' - use 'https://'`,
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* S017 Main Analyzer - Always use parameterized queries
|
|
3
|
-
*
|
|
4
|
-
* Fallback: Regex-based for all other cases
|
|
3
|
+
* Uses symbol-based analysis only (regex-based removed)
|
|
5
4
|
* Command: node cli.js --rule=S017 --input=examples/rule-test-fixtures/rules/S017_use_parameterized_queries --engine=heuristic
|
|
6
5
|
*/
|
|
7
6
|
|
|
8
7
|
const S017SymbolBasedAnalyzer = require("./symbol-based-analyzer.js");
|
|
9
|
-
const S017RegexBasedAnalyzer = require("./regex-based-analyzer.js");
|
|
10
8
|
|
|
11
9
|
class S017Analyzer {
|
|
12
10
|
constructor(options = {}) {
|
|
@@ -26,14 +24,7 @@ class S017Analyzer {
|
|
|
26
24
|
this.semanticEngine = options.semanticEngine || null;
|
|
27
25
|
this.verbose = options.verbose || false;
|
|
28
26
|
|
|
29
|
-
//
|
|
30
|
-
this.config = {
|
|
31
|
-
useSymbolBased: true, // Primary approach
|
|
32
|
-
fallbackToRegex: true, // Secondary approach
|
|
33
|
-
regexBasedOnly: false, // Can be set to true for pure mode
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
// Initialize analyzers
|
|
27
|
+
// Initialize symbol analyzer only
|
|
37
28
|
try {
|
|
38
29
|
this.symbolAnalyzer = new S017SymbolBasedAnalyzer(this.semanticEngine);
|
|
39
30
|
if (process.env.SUNLINT_DEBUG) {
|
|
@@ -43,15 +34,6 @@ class S017Analyzer {
|
|
|
43
34
|
console.error(`🔧 [S017] Error creating symbol analyzer:`, error);
|
|
44
35
|
}
|
|
45
36
|
|
|
46
|
-
try {
|
|
47
|
-
this.regexAnalyzer = new S017RegexBasedAnalyzer(this.semanticEngine);
|
|
48
|
-
if (process.env.SUNLINT_DEBUG) {
|
|
49
|
-
console.log(`🔧 [S017] Regex analyzer created successfully`);
|
|
50
|
-
}
|
|
51
|
-
} catch (error) {
|
|
52
|
-
console.error(`🔧 [S017] Error creating regex analyzer:`, error);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
37
|
if (process.env.SUNLINT_DEBUG) {
|
|
56
38
|
console.log(`🔧 [S017] Constructor completed`);
|
|
57
39
|
}
|
|
@@ -66,25 +48,19 @@ class S017Analyzer {
|
|
|
66
48
|
}
|
|
67
49
|
this.verbose = semanticEngine?.verbose || false;
|
|
68
50
|
|
|
69
|
-
// Initialize
|
|
51
|
+
// Initialize symbol analyzer
|
|
70
52
|
if (this.symbolAnalyzer) {
|
|
71
53
|
await this.symbolAnalyzer.initialize?.(semanticEngine);
|
|
72
54
|
}
|
|
73
|
-
if (this.regexAnalyzer) {
|
|
74
|
-
await this.regexAnalyzer.initialize?.(semanticEngine);
|
|
75
|
-
}
|
|
76
55
|
|
|
77
56
|
// Ensure verbose flag is propagated
|
|
78
|
-
if (this.regexAnalyzer) {
|
|
79
|
-
this.regexAnalyzer.verbose = this.verbose;
|
|
80
|
-
}
|
|
81
57
|
if (this.symbolAnalyzer) {
|
|
82
58
|
this.symbolAnalyzer.verbose = this.verbose;
|
|
83
59
|
}
|
|
84
60
|
|
|
85
61
|
if (this.verbose) {
|
|
86
62
|
console.log(
|
|
87
|
-
`🔧 [S017
|
|
63
|
+
`🔧 [S017] Analyzer initialized - verbose: ${this.verbose}`
|
|
88
64
|
);
|
|
89
65
|
}
|
|
90
66
|
}
|
|
@@ -147,15 +123,11 @@ class S017Analyzer {
|
|
|
147
123
|
// Create a Set to track unique violations and prevent duplicates
|
|
148
124
|
const violationMap = new Map();
|
|
149
125
|
|
|
150
|
-
//
|
|
151
|
-
if (
|
|
152
|
-
this.config.useSymbolBased &&
|
|
153
|
-
this.semanticEngine?.project &&
|
|
154
|
-
this.semanticEngine?.initialized
|
|
155
|
-
) {
|
|
126
|
+
// Symbol-based analysis only
|
|
127
|
+
if (this.semanticEngine?.project && this.semanticEngine?.initialized) {
|
|
156
128
|
try {
|
|
157
129
|
if (process.env.SUNLINT_DEBUG) {
|
|
158
|
-
console.log(`🔧 [S017]
|
|
130
|
+
console.log(`🔧 [S017] Running symbol-based analysis...`);
|
|
159
131
|
}
|
|
160
132
|
const sourceFile = this.semanticEngine.project.getSourceFile(filePath);
|
|
161
133
|
if (sourceFile) {
|
|
@@ -197,12 +169,10 @@ class S017Analyzer {
|
|
|
197
169
|
}
|
|
198
170
|
} catch (error) {
|
|
199
171
|
console.warn(`⚠️ [S017] Symbol analysis failed: ${error.message}`);
|
|
200
|
-
// Continue to fallback
|
|
201
172
|
}
|
|
202
173
|
} else {
|
|
203
174
|
if (process.env.SUNLINT_DEBUG) {
|
|
204
175
|
console.log(`🔄 [S017] Symbol analysis conditions check:`);
|
|
205
|
-
console.log(` - useSymbolBased: ${this.config.useSymbolBased}`);
|
|
206
176
|
console.log(` - semanticEngine: ${!!this.semanticEngine}`);
|
|
207
177
|
console.log(
|
|
208
178
|
` - semanticEngine.project: ${!!this.semanticEngine?.project}`
|
|
@@ -210,51 +180,14 @@ class S017Analyzer {
|
|
|
210
180
|
console.log(
|
|
211
181
|
` - semanticEngine.initialized: ${this.semanticEngine?.initialized}`
|
|
212
182
|
);
|
|
213
|
-
console.log(
|
|
214
|
-
`🔄 [S017] Symbol analysis unavailable, using regex fallback`
|
|
215
|
-
);
|
|
183
|
+
console.log(`🔄 [S017] Symbol analysis unavailable`);
|
|
216
184
|
}
|
|
217
185
|
}
|
|
218
186
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
try {
|
|
222
|
-
if (process.env.SUNLINT_DEBUG) {
|
|
223
|
-
console.log(`🔧 [S017] Trying regex-based analysis...`);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// Read file content for regex analyzer
|
|
227
|
-
const fs = require("fs");
|
|
228
|
-
const fileContent = fs.readFileSync(filePath, "utf8");
|
|
229
|
-
|
|
230
|
-
const violations = await this.regexAnalyzer.analyzeFile(
|
|
231
|
-
filePath,
|
|
232
|
-
fileContent,
|
|
233
|
-
options
|
|
234
|
-
);
|
|
235
|
-
|
|
236
|
-
// Add violations to map to deduplicate
|
|
237
|
-
violations.forEach((v) => {
|
|
238
|
-
const key = `${v.line}:${v.column}:${v.message}`;
|
|
239
|
-
if (!violationMap.has(key)) {
|
|
240
|
-
v.analysisStrategy = "regex-fallback";
|
|
241
|
-
violationMap.set(key, v);
|
|
242
|
-
}
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
if (process.env.SUNLINT_DEBUG) {
|
|
246
|
-
console.log(
|
|
247
|
-
`🔄 [S017] Regex-based analysis: ${violations.length} violations`
|
|
248
|
-
);
|
|
249
|
-
}
|
|
250
|
-
return Array.from(violationMap.values()); // Return deduplicated violations
|
|
251
|
-
} catch (error) {
|
|
252
|
-
console.error(`⚠ [S017] Regex analysis failed: ${error.message}`);
|
|
253
|
-
}
|
|
187
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
188
|
+
console.log(`🔧 [S017] Analysis completed: ${violationMap.size} violations`);
|
|
254
189
|
}
|
|
255
|
-
|
|
256
|
-
console.log(`🔧 [S017] No analysis methods succeeded, returning empty`);
|
|
257
|
-
return [];
|
|
190
|
+
return Array.from(violationMap.values());
|
|
258
191
|
}
|
|
259
192
|
|
|
260
193
|
/**
|