@sun-asterisk/sunlint 1.3.36 → 1.3.38
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/cli.js +34 -0
- package/config/rules/enhanced-rules-registry.json +387 -98
- package/config/rules/rules-registry-generated.json +202 -174
- package/config/rules-summary.json +1 -1
- package/core/architecture-integration.js +115 -17
- package/core/cli-action-handler.js +103 -28
- package/core/cli-program.js +7 -2
- package/core/github-annotate-service.js +62 -0
- package/core/impact-integration.js +31 -16
- package/core/init-command.js +261 -0
- package/core/output-service.js +64 -10
- package/core/performance-optimizer.js +1 -1
- package/core/summary-report-service.js +46 -0
- package/core/unified-rule-registry.js +4 -3
- package/docs/DART_RULE_EXECUTION_FLOW.md +1 -1
- package/docs/REGISTRY_GENERATION_DIAGRAM.md +289 -0
- package/docs/REGISTRY_GENERATION_FLOW.md +486 -0
- package/docs/skills/CREATE_NEW_DART_RULE.md +932 -0
- package/engines/eslint-engine.js +6 -0
- package/engines/heuristic-engine.js +23 -10
- package/engines/impact/core/detectors/database-detector.js +1 -1
- package/engines/impact/core/detectors/endpoint-detector.js +1 -1
- package/engines/impact/core/report-generator.js +235 -73
- package/origin-rules/dart-en.md +4 -4
- package/origin-rules/security-en.md +470 -282
- package/package.json +1 -1
- package/rules/dart/D001_recommended_lint_rules/config.json +134 -0
- package/rules/index.js +6 -4
- package/rules/security/S001_backend_auth_communications/dart/analyzer.js +44 -0
- package/rules/security/S001_backend_auth_communications/index.js +87 -0
- package/rules/security/S001_backend_auth_communications/typescript/analyzer.js +164 -0
- package/rules/security/S002_os_command_injection/dart/analyzer.js +44 -0
- package/rules/security/S002_os_command_injection/index.js +87 -0
- package/rules/security/S002_os_command_injection/typescript/analyzer.js +194 -0
- package/rules/security/S008_svg_content_validation/dart/analyzer.js +44 -0
- package/rules/security/S008_svg_content_validation/index.js +87 -0
- package/rules/security/S008_svg_content_validation/typescript/analyzer.js +216 -0
- package/rules/security/S018_no_sensitive_browser_storage/dart/analyzer.js +44 -0
- package/rules/security/S018_no_sensitive_browser_storage/index.js +86 -0
- package/rules/security/S018_no_sensitive_browser_storage/typescript/analyzer.js +193 -0
- package/rules/security/S021_referrer_policy/dart/analyzer.js +44 -0
- package/rules/security/S021_referrer_policy/index.js +86 -0
- package/rules/security/S021_referrer_policy/typescript/analyzer.js +183 -0
- package/rules/security/S023_no_json_injection/config.json +133 -44
- package/rules/security/S023_no_json_injection/dart/analyzer.js +7 -6
- package/rules/security/S023_no_json_injection/typescript/analyzer.js +402 -126
- package/rules/security/S023_no_json_injection/typescript/ast-analyzer.js +571 -154
- package/rules/security/S026_tls_all_connections/config.json +30 -0
- package/rules/security/S026_tls_all_connections/typescript/analyzer.js +339 -0
- package/rules/security/S027_mtls_certificate_validation/config.json +30 -0
- package/rules/security/S027_mtls_certificate_validation/typescript/analyzer.js +225 -0
- package/rules/security/S035_separate_app_hostnames/config.json +28 -0
- package/rules/security/S035_separate_app_hostnames/typescript/analyzer.js +186 -0
- package/rules/security/S036_lfi_rfi_protection/config.json +2 -2
- package/rules/security/S039_tls_certificate_validation/config.json +29 -0
- package/rules/security/S039_tls_certificate_validation/typescript/analyzer.js +229 -0
- package/rules/security/S046_jwt_algorithm_allowlist/config.json +28 -0
- package/rules/security/S046_jwt_algorithm_allowlist/dart/analyzer.js +44 -0
- package/rules/security/S046_jwt_algorithm_allowlist/index.js +87 -0
- package/rules/security/S046_jwt_algorithm_allowlist/typescript/analyzer.js +235 -0
- package/rules/security/S047_oauth_pkce_protection/config.json +31 -0
- package/rules/security/S047_oauth_pkce_protection/dart/analyzer.js +44 -0
- package/rules/security/S047_oauth_pkce_protection/index.js +86 -0
- package/rules/security/S047_oauth_pkce_protection/typescript/analyzer.js +78 -0
- package/rules/security/S048_oauth_redirect_uri_validation/config.json +30 -0
- package/rules/security/S048_oauth_redirect_uri_validation/typescript/analyzer.js +278 -0
- package/rules/security/S049_short_validity_tokens/typescript/config.json +10 -3
- package/rules/security/S050_reference_tokens_entropy/config.json +28 -0
- package/rules/security/S050_reference_tokens_entropy/dart/analyzer.js +45 -0
- package/rules/security/S050_reference_tokens_entropy/index.js +86 -0
- package/rules/security/S050_reference_tokens_entropy/typescript/analyzer.js +74 -0
- package/rules/security/S053_generic_error_messages/config.json +28 -0
- package/rules/security/S053_generic_error_messages/dart/analyzer.js +45 -0
- package/rules/security/S053_generic_error_messages/index.js +86 -0
- package/rules/security/S053_generic_error_messages/typescript/analyzer.js +80 -0
- package/rules/security/S055_content_type_validation/typescript/symbol-based-analyzer.js +64 -2
- package/rules/security/S059_disable_debug_mode/config.json +28 -0
- package/rules/security/S059_disable_debug_mode/dart/analyzer.js +45 -0
- package/rules/security/S059_disable_debug_mode/index.js +86 -0
- package/rules/security/S059_disable_debug_mode/typescript/analyzer.js +85 -0
- package/rules/security/S060_password_minimum_length/config.json +28 -0
- package/rules/security/S060_password_minimum_length/dart/analyzer.js +45 -0
- package/rules/security/S060_password_minimum_length/index.js +86 -0
- package/rules/security/S060_password_minimum_length/typescript/analyzer.js +78 -0
- package/rules/security/S026_json_schema_validation/config.json +0 -27
- package/rules/security/S026_json_schema_validation/typescript/analyzer.js +0 -251
- package/rules/security/S027_no_hardcoded_secrets/config.json +0 -29
- package/rules/security/S027_no_hardcoded_secrets/typescript/analyzer.js +0 -309
- package/rules/security/S027_no_hardcoded_secrets/typescript/categories.json +0 -153
- package/rules/security/S035_path_session_cookies/config.json +0 -99
- package/rules/security/S035_path_session_cookies/typescript/analyzer.js +0 -316
- package/rules/security/S035_path_session_cookies/typescript/regex-based-analyzer.js +0 -724
- package/rules/security/S035_path_session_cookies/typescript/symbol-based-analyzer.js +0 -373
- package/rules/security/S039_no_session_tokens_in_url/config.json +0 -92
- package/rules/security/S039_no_session_tokens_in_url/typescript/analyzer.js +0 -262
- package/rules/security/S039_no_session_tokens_in_url/typescript/regex-based-analyzer.js +0 -337
- package/rules/security/S039_no_session_tokens_in_url/typescript/symbol-based-analyzer.js +0 -443
- package/rules/security/S048_no_current_password_in_reset/config.json +0 -48
- package/rules/security/S048_no_current_password_in_reset/typescript/analyzer.js +0 -366
- /package/rules/security/{S026_json_schema_validation → S026_tls_all_connections}/dart/analyzer.js +0 -0
- /package/rules/security/{S026_json_schema_validation → S026_tls_all_connections}/index.js +0 -0
- /package/rules/security/{S027_no_hardcoded_secrets → S027_mtls_certificate_validation}/dart/analyzer.js +0 -0
- /package/rules/security/{S027_no_hardcoded_secrets → S027_mtls_certificate_validation}/index.js +0 -0
- /package/rules/security/{S027_no_hardcoded_secrets → S027_mtls_certificate_validation}/typescript/categorized-analyzer.js +0 -0
- /package/rules/security/{S035_path_session_cookies → S035_separate_app_hostnames}/dart/analyzer.js +0 -0
- /package/rules/security/{S035_path_session_cookies → S035_separate_app_hostnames}/index.js +0 -0
- /package/rules/security/{S035_path_session_cookies → S035_separate_app_hostnames}/typescript/README.md +0 -0
- /package/rules/security/{S039_no_session_tokens_in_url → S039_tls_certificate_validation}/dart/analyzer.js +0 -0
- /package/rules/security/{S039_no_session_tokens_in_url → S039_tls_certificate_validation}/index.js +0 -0
- /package/rules/security/{S039_no_session_tokens_in_url → S039_tls_certificate_validation}/typescript/README.md +0 -0
- /package/rules/security/{S048_no_current_password_in_reset → S048_oauth_redirect_uri_validation}/dart/analyzer.js +0 -0
- /package/rules/security/{S048_no_current_password_in_reset → S048_oauth_redirect_uri_validation}/index.js +0 -0
- /package/rules/security/{S048_no_current_password_in_reset → S048_oauth_redirect_uri_validation}/typescript/README.md +0 -0
|
@@ -1,262 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* S039 Main Analyzer - Do not pass Session Tokens via URL parameters
|
|
3
|
-
* Primary: Symbol-based analysis (when available)
|
|
4
|
-
* Fallback: Regex-based for all other cases
|
|
5
|
-
* Command: node cli.js --rule=S039 --input=examples/rule-test-fixtures/rules/S039_no_session_tokens_in_url --engine=heuristic
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
const S039SymbolBasedAnalyzer = require("./symbol-based-analyzer.js");
|
|
9
|
-
const S039RegexBasedAnalyzer = require("./regex-based-analyzer.js");
|
|
10
|
-
|
|
11
|
-
class S039Analyzer {
|
|
12
|
-
constructor(options = {}) {
|
|
13
|
-
if (process.env.SUNLINT_DEBUG) {
|
|
14
|
-
console.log(`🔧 [S039] Constructor called with options:`, !!options);
|
|
15
|
-
console.log(
|
|
16
|
-
`🔧 [S039] Options type:`,
|
|
17
|
-
typeof options,
|
|
18
|
-
Object.keys(options || {})
|
|
19
|
-
);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
this.ruleId = "S039";
|
|
23
|
-
this.ruleName = "Do not pass Session Tokens via URL parameters";
|
|
24
|
-
this.description =
|
|
25
|
-
"Detects when session tokens, authentication tokens, JWT tokens, or other sensitive authentication data are passed as URL parameters instead of secure headers or body. URL parameters are logged in web server logs, browser history, and can be exposed in referrer headers.";
|
|
26
|
-
this.semanticEngine = options.semanticEngine || null;
|
|
27
|
-
this.verbose = options.verbose || false;
|
|
28
|
-
|
|
29
|
-
this.config = {
|
|
30
|
-
useSymbolBased: true,
|
|
31
|
-
fallbackToRegex: true,
|
|
32
|
-
regexBasedOnly: false,
|
|
33
|
-
prioritizeSymbolic: true, // Prefer symbol-based when available
|
|
34
|
-
fallbackToSymbol: true, // Allow symbol analysis even without semantic engine
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
try {
|
|
38
|
-
this.symbolAnalyzer = new S039SymbolBasedAnalyzer(this.semanticEngine);
|
|
39
|
-
if (process.env.SUNLINT_DEBUG)
|
|
40
|
-
console.log(`🔧 [S039] Symbol analyzer created successfully`);
|
|
41
|
-
} catch (error) {
|
|
42
|
-
console.error(`🔧 [S039] Error creating symbol analyzer:`, error);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
try {
|
|
46
|
-
this.regexAnalyzer = new S039RegexBasedAnalyzer(this.semanticEngine);
|
|
47
|
-
if (process.env.SUNLINT_DEBUG)
|
|
48
|
-
console.log(`🔧 [S039] Regex analyzer created successfully`);
|
|
49
|
-
} catch (error) {
|
|
50
|
-
console.error(`🔧 [S039] Error creating regex analyzer:`, error);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
async initialize(semanticEngine) {
|
|
55
|
-
this.semanticEngine = semanticEngine;
|
|
56
|
-
if (process.env.SUNLINT_DEBUG)
|
|
57
|
-
console.log(`🔧 [S039] Main analyzer initializing...`);
|
|
58
|
-
|
|
59
|
-
if (this.symbolAnalyzer)
|
|
60
|
-
await this.symbolAnalyzer.initialize?.(semanticEngine);
|
|
61
|
-
if (this.regexAnalyzer)
|
|
62
|
-
await this.regexAnalyzer.initialize?.(semanticEngine);
|
|
63
|
-
if (this.regexAnalyzer) this.regexAnalyzer.cleanup?.();
|
|
64
|
-
|
|
65
|
-
if (process.env.SUNLINT_DEBUG)
|
|
66
|
-
console.log(`🔧 [S039] Main analyzer initialized successfully`);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
analyzeSingle(filePath, options = {}) {
|
|
70
|
-
if (process.env.SUNLINT_DEBUG)
|
|
71
|
-
console.log(`🔍 [S039] analyzeSingle() called for: ${filePath}`);
|
|
72
|
-
return this.analyze([filePath], "typescript", options);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
async analyze(files, language, options = {}) {
|
|
76
|
-
if (process.env.SUNLINT_DEBUG) {
|
|
77
|
-
console.log(
|
|
78
|
-
`🔧 [S039] analyze() method called with ${files.length} files, language: ${language}`
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const violations = [];
|
|
83
|
-
for (const filePath of files) {
|
|
84
|
-
try {
|
|
85
|
-
if (process.env.SUNLINT_DEBUG)
|
|
86
|
-
console.log(`🔧 [S039] Processing file: ${filePath}`);
|
|
87
|
-
const fileViolations = await this.analyzeFile(filePath, options);
|
|
88
|
-
violations.push(...fileViolations);
|
|
89
|
-
if (process.env.SUNLINT_DEBUG)
|
|
90
|
-
console.log(
|
|
91
|
-
`🔧 [S039] File ${filePath}: Found ${fileViolations.length} violations`
|
|
92
|
-
);
|
|
93
|
-
} catch (error) {
|
|
94
|
-
console.warn(
|
|
95
|
-
`⚠ [S039] Analysis failed for ${filePath}:`,
|
|
96
|
-
error.message
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (process.env.SUNLINT_DEBUG)
|
|
102
|
-
console.log(`🔧 [S039] Total violations found: ${violations.length}`);
|
|
103
|
-
return violations;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
async analyzeFile(filePath, options = {}) {
|
|
107
|
-
if (process.env.SUNLINT_DEBUG)
|
|
108
|
-
console.log(`🔍 [S039] analyzeFile() called for: ${filePath}`);
|
|
109
|
-
const violationMap = new Map();
|
|
110
|
-
|
|
111
|
-
// Try symbol-based analysis first when semantic engine is available OR when explicitly enabled
|
|
112
|
-
if (process.env.SUNLINT_DEBUG) {
|
|
113
|
-
console.log(
|
|
114
|
-
`🔧 [S039] Symbol check: useSymbolBased=${
|
|
115
|
-
this.config.useSymbolBased
|
|
116
|
-
}, semanticEngine=${!!this.semanticEngine}, project=${!!this
|
|
117
|
-
.semanticEngine?.project}, initialized=${!!this.semanticEngine
|
|
118
|
-
?.initialized}`
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const canUseSymbol =
|
|
123
|
-
this.config.useSymbolBased &&
|
|
124
|
-
((this.semanticEngine?.project && this.semanticEngine?.initialized) ||
|
|
125
|
-
(!this.semanticEngine && this.config.fallbackToSymbol !== false));
|
|
126
|
-
|
|
127
|
-
if (canUseSymbol) {
|
|
128
|
-
try {
|
|
129
|
-
if (process.env.SUNLINT_DEBUG)
|
|
130
|
-
console.log(`🔧 [S039] Trying symbol-based analysis...`);
|
|
131
|
-
|
|
132
|
-
let sourceFile = null;
|
|
133
|
-
if (this.semanticEngine?.project) {
|
|
134
|
-
sourceFile = this.semanticEngine.project.getSourceFile(filePath);
|
|
135
|
-
if (process.env.SUNLINT_DEBUG) {
|
|
136
|
-
console.log(
|
|
137
|
-
`🔧 [S039] Checked existing semantic engine project: sourceFile=${!!sourceFile}`
|
|
138
|
-
);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
if (!sourceFile) {
|
|
143
|
-
// Create a minimal ts-morph project for this analysis
|
|
144
|
-
if (process.env.SUNLINT_DEBUG)
|
|
145
|
-
console.log(
|
|
146
|
-
`🔧 [S039] Creating temporary ts-morph project for: ${filePath}`
|
|
147
|
-
);
|
|
148
|
-
try {
|
|
149
|
-
const fs = require("fs");
|
|
150
|
-
const path = require("path");
|
|
151
|
-
const { Project } = require("ts-morph");
|
|
152
|
-
|
|
153
|
-
// Check if file exists and read content
|
|
154
|
-
if (!fs.existsSync(filePath)) {
|
|
155
|
-
throw new Error(`File not found: ${filePath}`);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const fileContent = fs.readFileSync(filePath, "utf8");
|
|
159
|
-
const fileName = path.basename(filePath);
|
|
160
|
-
|
|
161
|
-
const tempProject = new Project({
|
|
162
|
-
useInMemoryFileSystem: true,
|
|
163
|
-
compilerOptions: {
|
|
164
|
-
allowJs: true,
|
|
165
|
-
allowSyntheticDefaultImports: true,
|
|
166
|
-
},
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
// Add file content to in-memory project
|
|
170
|
-
sourceFile = tempProject.createSourceFile(fileName, fileContent);
|
|
171
|
-
if (process.env.SUNLINT_DEBUG)
|
|
172
|
-
console.log(
|
|
173
|
-
`🔧 [S039] Temporary project created successfully with file: ${fileName}`
|
|
174
|
-
);
|
|
175
|
-
} catch (projectError) {
|
|
176
|
-
if (process.env.SUNLINT_DEBUG)
|
|
177
|
-
console.log(
|
|
178
|
-
`🔧 [S039] Failed to create temporary project:`,
|
|
179
|
-
projectError.message
|
|
180
|
-
);
|
|
181
|
-
throw projectError;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
if (sourceFile) {
|
|
186
|
-
const symbolViolations = await this.symbolAnalyzer.analyze(
|
|
187
|
-
sourceFile,
|
|
188
|
-
filePath
|
|
189
|
-
);
|
|
190
|
-
symbolViolations.forEach((v) => {
|
|
191
|
-
const key = `${v.line}:${v.column}:${v.message}`;
|
|
192
|
-
if (!violationMap.has(key)) violationMap.set(key, v);
|
|
193
|
-
});
|
|
194
|
-
if (process.env.SUNLINT_DEBUG)
|
|
195
|
-
console.log(
|
|
196
|
-
`🔧 [S039] Symbol analysis completed: ${symbolViolations.length} violations`
|
|
197
|
-
);
|
|
198
|
-
|
|
199
|
-
// If symbol-based found violations or prioritizeSymbolic is true, skip regex
|
|
200
|
-
if (this.config.prioritizeSymbolic && symbolViolations.length >= 0) {
|
|
201
|
-
const finalViolations = Array.from(violationMap.values()).map(
|
|
202
|
-
(v) => ({
|
|
203
|
-
...v,
|
|
204
|
-
filePath,
|
|
205
|
-
file: filePath,
|
|
206
|
-
})
|
|
207
|
-
);
|
|
208
|
-
if (process.env.SUNLINT_DEBUG)
|
|
209
|
-
console.log(
|
|
210
|
-
`🔧 [S039] Symbol-based analysis prioritized: ${finalViolations.length} violations`
|
|
211
|
-
);
|
|
212
|
-
return finalViolations;
|
|
213
|
-
}
|
|
214
|
-
} else {
|
|
215
|
-
if (process.env.SUNLINT_DEBUG)
|
|
216
|
-
console.log(
|
|
217
|
-
`🔧 [S039] No source file found, skipping symbol analysis`
|
|
218
|
-
);
|
|
219
|
-
}
|
|
220
|
-
} catch (error) {
|
|
221
|
-
console.warn(`⚠ [S039] Symbol analysis failed:`, error.message);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// Fallback to regex-based analysis
|
|
226
|
-
if (this.config.fallbackToRegex || this.config.regexBasedOnly) {
|
|
227
|
-
try {
|
|
228
|
-
if (process.env.SUNLINT_DEBUG)
|
|
229
|
-
console.log(`🔧 [S039] Trying regex-based analysis...`);
|
|
230
|
-
const regexViolations = await this.regexAnalyzer.analyze(filePath);
|
|
231
|
-
regexViolations.forEach((v) => {
|
|
232
|
-
const key = `${v.line}:${v.column}:${v.message}`;
|
|
233
|
-
if (!violationMap.has(key)) violationMap.set(key, v);
|
|
234
|
-
});
|
|
235
|
-
if (process.env.SUNLINT_DEBUG)
|
|
236
|
-
console.log(
|
|
237
|
-
`🔧 [S039] Regex analysis completed: ${regexViolations.length} violations`
|
|
238
|
-
);
|
|
239
|
-
} catch (error) {
|
|
240
|
-
console.warn(`⚠ [S039] Regex analysis failed:`, error.message);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
const finalViolations = Array.from(violationMap.values()).map((v) => ({
|
|
245
|
-
...v,
|
|
246
|
-
filePath,
|
|
247
|
-
file: filePath,
|
|
248
|
-
}));
|
|
249
|
-
if (process.env.SUNLINT_DEBUG)
|
|
250
|
-
console.log(
|
|
251
|
-
`🔧 [S039] File analysis completed: ${finalViolations.length} unique violations`
|
|
252
|
-
);
|
|
253
|
-
return finalViolations;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
cleanup() {
|
|
257
|
-
if (this.symbolAnalyzer?.cleanup) this.symbolAnalyzer.cleanup();
|
|
258
|
-
if (this.regexAnalyzer?.cleanup) this.regexAnalyzer.cleanup();
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
module.exports = S039Analyzer;
|
|
@@ -1,337 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* S039 Regex-Based Analyzer - Do not pass Session Tokens via URL parameters
|
|
3
|
-
* Detects session token exposure in URL parameters across different frameworks
|
|
4
|
-
*/
|
|
5
|
-
const fs = require("fs");
|
|
6
|
-
|
|
7
|
-
class S039RegexBasedAnalyzer {
|
|
8
|
-
constructor() {
|
|
9
|
-
this.ruleId = "S039";
|
|
10
|
-
|
|
11
|
-
// Framework-specific route patterns
|
|
12
|
-
this.routePatterns = {
|
|
13
|
-
// Express.js
|
|
14
|
-
express:
|
|
15
|
-
/\b(app|router)\.(get|post|put|delete|patch|use)\s*\(\s*['"`][^'"`]+['"`]/,
|
|
16
|
-
// Next.js API routes (legacy handler)
|
|
17
|
-
nextjs: /export\s+(default\s+)?async?\s+function\s+handler\s*\(/,
|
|
18
|
-
// Next.js 13+ App Router HTTP methods
|
|
19
|
-
nextjsApp:
|
|
20
|
-
/export\s+async?\s+function\s+(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s*\(/,
|
|
21
|
-
// NestJS controllers
|
|
22
|
-
nestjs: /@(Get|Post|Put|Delete|Patch)\s*\(\s*['"`][^'"`]*['"`]?\s*\)/,
|
|
23
|
-
// Nuxt.js server routes
|
|
24
|
-
nuxtjs:
|
|
25
|
-
/export\s+(default\s+|const\s+(GET|POST|PUT|DELETE|PATCH)\s*=\s*)?defineEventHandler\s*\(/,
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
// Session token parameter names to detect
|
|
29
|
-
this.sessionTokenParams = [
|
|
30
|
-
"sessionId",
|
|
31
|
-
"session_id",
|
|
32
|
-
"session-id",
|
|
33
|
-
"sessionToken",
|
|
34
|
-
"session_token",
|
|
35
|
-
"session-token",
|
|
36
|
-
"authToken",
|
|
37
|
-
"auth_token",
|
|
38
|
-
"auth-token",
|
|
39
|
-
"authorization",
|
|
40
|
-
"bearer",
|
|
41
|
-
"jwt",
|
|
42
|
-
"jwtToken",
|
|
43
|
-
"jwt_token",
|
|
44
|
-
"jwt-token",
|
|
45
|
-
"accessToken",
|
|
46
|
-
"access_token",
|
|
47
|
-
"access-token",
|
|
48
|
-
"refreshToken",
|
|
49
|
-
"refresh_token",
|
|
50
|
-
"refresh-token",
|
|
51
|
-
"apiKey",
|
|
52
|
-
"api_key",
|
|
53
|
-
"api-key",
|
|
54
|
-
"csrfToken",
|
|
55
|
-
"csrf_token",
|
|
56
|
-
"csrf-token",
|
|
57
|
-
"xsrfToken",
|
|
58
|
-
"xsrf_token",
|
|
59
|
-
"xsrf-token",
|
|
60
|
-
"token",
|
|
61
|
-
"apiToken",
|
|
62
|
-
"api_token",
|
|
63
|
-
"api-token",
|
|
64
|
-
"sid",
|
|
65
|
-
"sessionkey",
|
|
66
|
-
"session_key",
|
|
67
|
-
"session-key",
|
|
68
|
-
"userToken",
|
|
69
|
-
"user_token",
|
|
70
|
-
"user-token",
|
|
71
|
-
"authKey",
|
|
72
|
-
"auth_key",
|
|
73
|
-
"auth-key",
|
|
74
|
-
"securityToken",
|
|
75
|
-
"security_token",
|
|
76
|
-
"security-token",
|
|
77
|
-
];
|
|
78
|
-
|
|
79
|
-
// URL parameter access patterns for different frameworks
|
|
80
|
-
this.urlParamPatterns = {
|
|
81
|
-
// Express.js: req.query.sessionToken, req.params.authToken
|
|
82
|
-
express: /req\.(query|params)\.(\w+)/g,
|
|
83
|
-
// Express.js bracket notation: req.query["access-token"]
|
|
84
|
-
expressBracket: /req\.(query|params)\[['"`]([^'"`]+)['"`]\]/g,
|
|
85
|
-
// NestJS: @Query('sessionToken'), @Param('authToken')
|
|
86
|
-
nestjs: /@(Query|Param)\s*\(\s*['"`](\w+)['"`]\s*\)/g,
|
|
87
|
-
// Next.js: searchParams.get('sessionToken')
|
|
88
|
-
nextjs: /searchParams\.get\s*\(\s*['"`](\w+)['"`]\s*\)/g,
|
|
89
|
-
// Generic destructuring: { sessionToken } = req.query
|
|
90
|
-
destructuring: /\{\s*([^}]+)\s*\}\s*=\s*req\.(query|params)/g,
|
|
91
|
-
// URL constructor patterns: new URL().searchParams.get()
|
|
92
|
-
urlConstructor:
|
|
93
|
-
/new\s+URL\([^)]+\)\.searchParams\.get\s*\(\s*['"`](\w+)['"`]\s*\)/g,
|
|
94
|
-
// URLSearchParams patterns: params.get("token")
|
|
95
|
-
urlSearchParams: /(\w+)\.get\s*\(\s*['"`](\w+)['"`]\s*\)/g,
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
// Pattern to detect session token-like values
|
|
99
|
-
this.tokenValuePattern = /^[a-zA-Z0-9+/=\-_.]{16,}$/;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
async analyze(filePath) {
|
|
103
|
-
// Skip files that are unlikely to be route handlers
|
|
104
|
-
const skipPatterns = [
|
|
105
|
-
/\.dto\.ts$/,
|
|
106
|
-
/\.interface\.ts$/,
|
|
107
|
-
/\.module\.ts$/,
|
|
108
|
-
/\.service\.spec\.ts$/,
|
|
109
|
-
/\.controller\.spec\.ts$/,
|
|
110
|
-
/\.spec\.ts$/,
|
|
111
|
-
/\.test\.ts$/,
|
|
112
|
-
/\.d\.ts$/,
|
|
113
|
-
/\.types\.ts$/,
|
|
114
|
-
/\.constants?\.ts$/,
|
|
115
|
-
/\.config\.ts$/,
|
|
116
|
-
];
|
|
117
|
-
|
|
118
|
-
const shouldSkip = skipPatterns.some((pattern) => pattern.test(filePath));
|
|
119
|
-
if (shouldSkip) {
|
|
120
|
-
return [];
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const content = fs.readFileSync(filePath, "utf8");
|
|
124
|
-
const lines = content.split(/\r?\n/);
|
|
125
|
-
const violations = [];
|
|
126
|
-
|
|
127
|
-
let inRoute = false;
|
|
128
|
-
let braceDepth = 0;
|
|
129
|
-
let routeStartLine = 0;
|
|
130
|
-
let routeType = null;
|
|
131
|
-
const tokenExposures = [];
|
|
132
|
-
|
|
133
|
-
// Helper functions
|
|
134
|
-
const reset = () => {
|
|
135
|
-
inRoute = false;
|
|
136
|
-
braceDepth = 0;
|
|
137
|
-
routeStartLine = 0;
|
|
138
|
-
routeType = null;
|
|
139
|
-
tokenExposures.length = 0;
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
const evaluate = () => {
|
|
143
|
-
// Report violations for exposed session tokens
|
|
144
|
-
for (const exposure of tokenExposures) {
|
|
145
|
-
violations.push({
|
|
146
|
-
ruleId: this.ruleId,
|
|
147
|
-
message: `Session token '${exposure.paramName}' passed via URL parameter - use secure headers or request body instead`,
|
|
148
|
-
severity: "warning",
|
|
149
|
-
line: exposure.line,
|
|
150
|
-
column: 1,
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
for (let i = 0; i < lines.length; i++) {
|
|
156
|
-
const line = lines[i];
|
|
157
|
-
|
|
158
|
-
// Detect route start for different frameworks
|
|
159
|
-
if (!inRoute) {
|
|
160
|
-
for (const [framework, pattern] of Object.entries(this.routePatterns)) {
|
|
161
|
-
if (pattern.test(line)) {
|
|
162
|
-
if (process.env.SUNLINT_DEBUG)
|
|
163
|
-
console.log(
|
|
164
|
-
`🔧 [S039-Regex] Found ${framework} route at line ${
|
|
165
|
-
i + 1
|
|
166
|
-
}: ${line.trim()}`
|
|
167
|
-
);
|
|
168
|
-
inRoute = true;
|
|
169
|
-
routeStartLine = i + 1;
|
|
170
|
-
routeType = framework;
|
|
171
|
-
braceDepth =
|
|
172
|
-
(line.match(/\{/g) || []).length -
|
|
173
|
-
(line.match(/\}/g) || []).length;
|
|
174
|
-
|
|
175
|
-
// If no opening brace on this line, look ahead for it
|
|
176
|
-
if (braceDepth === 0) {
|
|
177
|
-
for (let j = i + 1; j < Math.min(i + 3, lines.length); j++) {
|
|
178
|
-
const nextLine = lines[j];
|
|
179
|
-
if (nextLine.includes("{")) {
|
|
180
|
-
braceDepth =
|
|
181
|
-
(nextLine.match(/\{/g) || []).length -
|
|
182
|
-
(nextLine.match(/\}/g) || []).length;
|
|
183
|
-
break;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
break;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
if (inRoute) {
|
|
193
|
-
// Update brace depth
|
|
194
|
-
braceDepth +=
|
|
195
|
-
(line.match(/\{/g) || []).length - (line.match(/\}/g) || []).length;
|
|
196
|
-
|
|
197
|
-
// Check for session token parameter access
|
|
198
|
-
this.checkTokenParameterAccess(line, i + 1, tokenExposures);
|
|
199
|
-
|
|
200
|
-
// End of route detection
|
|
201
|
-
if (
|
|
202
|
-
braceDepth <= 0 &&
|
|
203
|
-
(/\)\s*;?\s*$/.test(line) ||
|
|
204
|
-
/^\s*\}\s*$/.test(line) ||
|
|
205
|
-
/^export/.test(line))
|
|
206
|
-
) {
|
|
207
|
-
if (process.env.SUNLINT_DEBUG) {
|
|
208
|
-
console.log(
|
|
209
|
-
`🔧 [S039-Regex] Route ended, evaluating: Token exposures=${tokenExposures.length}`
|
|
210
|
-
);
|
|
211
|
-
}
|
|
212
|
-
evaluate();
|
|
213
|
-
reset();
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// Safety evaluate if unbalanced at file end
|
|
219
|
-
if (inRoute) evaluate();
|
|
220
|
-
|
|
221
|
-
return violations;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
checkTokenParameterAccess(line, lineNumber, exposures) {
|
|
225
|
-
// Check each URL parameter access pattern
|
|
226
|
-
for (const [framework, pattern] of Object.entries(this.urlParamPatterns)) {
|
|
227
|
-
const matches = [...line.matchAll(pattern)];
|
|
228
|
-
|
|
229
|
-
for (const match of matches) {
|
|
230
|
-
let paramName = null;
|
|
231
|
-
|
|
232
|
-
if (framework === "express") {
|
|
233
|
-
// req.query.sessionToken or req.params.authToken
|
|
234
|
-
paramName = match[2];
|
|
235
|
-
} else if (framework === "expressBracket") {
|
|
236
|
-
// req.query["access-token"] or req.params["auth-token"]
|
|
237
|
-
paramName = match[2];
|
|
238
|
-
} else if (framework === "nestjs") {
|
|
239
|
-
// @Query('sessionToken') or @Param('authToken')
|
|
240
|
-
paramName = match[2];
|
|
241
|
-
} else if (framework === "nextjs" || framework === "urlConstructor") {
|
|
242
|
-
// searchParams.get('sessionToken'), new URL().searchParams.get('token')
|
|
243
|
-
paramName = match[1];
|
|
244
|
-
} else if (framework === "urlSearchParams") {
|
|
245
|
-
// params.get('sessionToken') - general URLSearchParams pattern
|
|
246
|
-
paramName = match[2];
|
|
247
|
-
} else if (framework === "destructuring") {
|
|
248
|
-
// { sessionToken, authToken } = req.query
|
|
249
|
-
const destructuredParams = match[1].split(",").map((p) => p.trim());
|
|
250
|
-
for (const param of destructuredParams) {
|
|
251
|
-
const cleanParam = param.replace(/['"]/g, "");
|
|
252
|
-
if (this.isSessionTokenParam(cleanParam)) {
|
|
253
|
-
exposures.push({
|
|
254
|
-
paramName: cleanParam,
|
|
255
|
-
line: lineNumber,
|
|
256
|
-
framework: framework,
|
|
257
|
-
accessType: match[2], // query or params
|
|
258
|
-
});
|
|
259
|
-
if (process.env.SUNLINT_DEBUG) {
|
|
260
|
-
console.log(
|
|
261
|
-
`🔧 [S039-Regex] Found token parameter exposure: ${cleanParam} via ${framework}`
|
|
262
|
-
);
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
continue; // Skip the normal parameter check below
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
if (paramName && this.isSessionTokenParam(paramName)) {
|
|
270
|
-
exposures.push({
|
|
271
|
-
paramName: paramName,
|
|
272
|
-
line: lineNumber,
|
|
273
|
-
framework: framework,
|
|
274
|
-
accessType: framework === "express" ? match[1] : "parameter",
|
|
275
|
-
});
|
|
276
|
-
if (process.env.SUNLINT_DEBUG) {
|
|
277
|
-
console.log(
|
|
278
|
-
`🔧 [S039-Regex] Found token parameter exposure: ${paramName} via ${framework}`
|
|
279
|
-
);
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// Additional patterns for hardcoded URL parsing
|
|
286
|
-
const hardcodedPatterns = [
|
|
287
|
-
// window.location.search, location.search
|
|
288
|
-
/(?:window\.)?location\.search/g,
|
|
289
|
-
// URLSearchParams(location.search)
|
|
290
|
-
/URLSearchParams\s*\(\s*(?:window\.)?location\.search\s*\)/g,
|
|
291
|
-
// document.location.search
|
|
292
|
-
/document\.location\.search/g,
|
|
293
|
-
];
|
|
294
|
-
|
|
295
|
-
for (const pattern of hardcodedPatterns) {
|
|
296
|
-
if (pattern.test(line)) {
|
|
297
|
-
// Look for subsequent .get() calls or parameter access in nearby lines
|
|
298
|
-
for (
|
|
299
|
-
let j = Math.max(0, lineNumber - 3);
|
|
300
|
-
j < Math.min(lineNumber + 3, exposures.length + lineNumber);
|
|
301
|
-
j++
|
|
302
|
-
) {
|
|
303
|
-
const nearbyLine = exposures[j] || line;
|
|
304
|
-
const getMatches = [
|
|
305
|
-
...nearbyLine.matchAll(/\.get\s*\(\s*['"`](\w+)['"`]\s*\)/g),
|
|
306
|
-
];
|
|
307
|
-
for (const getMatch of getMatches) {
|
|
308
|
-
const paramName = getMatch[1];
|
|
309
|
-
if (this.isSessionTokenParam(paramName)) {
|
|
310
|
-
exposures.push({
|
|
311
|
-
paramName: paramName,
|
|
312
|
-
line: lineNumber,
|
|
313
|
-
framework: "client-side",
|
|
314
|
-
accessType: "url-search",
|
|
315
|
-
});
|
|
316
|
-
if (process.env.SUNLINT_DEBUG) {
|
|
317
|
-
console.log(
|
|
318
|
-
`🔧 [S039-Regex] Found client-side token parameter: ${paramName}`
|
|
319
|
-
);
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
isSessionTokenParam(paramName) {
|
|
329
|
-
return this.sessionTokenParams.some(
|
|
330
|
-
(tokenParam) => tokenParam.toLowerCase() === paramName.toLowerCase()
|
|
331
|
-
);
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
cleanup() {}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
module.exports = S039RegexBasedAnalyzer;
|