@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.
Files changed (113) hide show
  1. package/cli.js +34 -0
  2. package/config/rules/enhanced-rules-registry.json +387 -98
  3. package/config/rules/rules-registry-generated.json +202 -174
  4. package/config/rules-summary.json +1 -1
  5. package/core/architecture-integration.js +115 -17
  6. package/core/cli-action-handler.js +103 -28
  7. package/core/cli-program.js +7 -2
  8. package/core/github-annotate-service.js +62 -0
  9. package/core/impact-integration.js +31 -16
  10. package/core/init-command.js +261 -0
  11. package/core/output-service.js +64 -10
  12. package/core/performance-optimizer.js +1 -1
  13. package/core/summary-report-service.js +46 -0
  14. package/core/unified-rule-registry.js +4 -3
  15. package/docs/DART_RULE_EXECUTION_FLOW.md +1 -1
  16. package/docs/REGISTRY_GENERATION_DIAGRAM.md +289 -0
  17. package/docs/REGISTRY_GENERATION_FLOW.md +486 -0
  18. package/docs/skills/CREATE_NEW_DART_RULE.md +932 -0
  19. package/engines/eslint-engine.js +6 -0
  20. package/engines/heuristic-engine.js +23 -10
  21. package/engines/impact/core/detectors/database-detector.js +1 -1
  22. package/engines/impact/core/detectors/endpoint-detector.js +1 -1
  23. package/engines/impact/core/report-generator.js +235 -73
  24. package/origin-rules/dart-en.md +4 -4
  25. package/origin-rules/security-en.md +470 -282
  26. package/package.json +1 -1
  27. package/rules/dart/D001_recommended_lint_rules/config.json +134 -0
  28. package/rules/index.js +6 -4
  29. package/rules/security/S001_backend_auth_communications/dart/analyzer.js +44 -0
  30. package/rules/security/S001_backend_auth_communications/index.js +87 -0
  31. package/rules/security/S001_backend_auth_communications/typescript/analyzer.js +164 -0
  32. package/rules/security/S002_os_command_injection/dart/analyzer.js +44 -0
  33. package/rules/security/S002_os_command_injection/index.js +87 -0
  34. package/rules/security/S002_os_command_injection/typescript/analyzer.js +194 -0
  35. package/rules/security/S008_svg_content_validation/dart/analyzer.js +44 -0
  36. package/rules/security/S008_svg_content_validation/index.js +87 -0
  37. package/rules/security/S008_svg_content_validation/typescript/analyzer.js +216 -0
  38. package/rules/security/S018_no_sensitive_browser_storage/dart/analyzer.js +44 -0
  39. package/rules/security/S018_no_sensitive_browser_storage/index.js +86 -0
  40. package/rules/security/S018_no_sensitive_browser_storage/typescript/analyzer.js +193 -0
  41. package/rules/security/S021_referrer_policy/dart/analyzer.js +44 -0
  42. package/rules/security/S021_referrer_policy/index.js +86 -0
  43. package/rules/security/S021_referrer_policy/typescript/analyzer.js +183 -0
  44. package/rules/security/S023_no_json_injection/config.json +133 -44
  45. package/rules/security/S023_no_json_injection/dart/analyzer.js +7 -6
  46. package/rules/security/S023_no_json_injection/typescript/analyzer.js +402 -126
  47. package/rules/security/S023_no_json_injection/typescript/ast-analyzer.js +571 -154
  48. package/rules/security/S026_tls_all_connections/config.json +30 -0
  49. package/rules/security/S026_tls_all_connections/typescript/analyzer.js +339 -0
  50. package/rules/security/S027_mtls_certificate_validation/config.json +30 -0
  51. package/rules/security/S027_mtls_certificate_validation/typescript/analyzer.js +225 -0
  52. package/rules/security/S035_separate_app_hostnames/config.json +28 -0
  53. package/rules/security/S035_separate_app_hostnames/typescript/analyzer.js +186 -0
  54. package/rules/security/S036_lfi_rfi_protection/config.json +2 -2
  55. package/rules/security/S039_tls_certificate_validation/config.json +29 -0
  56. package/rules/security/S039_tls_certificate_validation/typescript/analyzer.js +229 -0
  57. package/rules/security/S046_jwt_algorithm_allowlist/config.json +28 -0
  58. package/rules/security/S046_jwt_algorithm_allowlist/dart/analyzer.js +44 -0
  59. package/rules/security/S046_jwt_algorithm_allowlist/index.js +87 -0
  60. package/rules/security/S046_jwt_algorithm_allowlist/typescript/analyzer.js +235 -0
  61. package/rules/security/S047_oauth_pkce_protection/config.json +31 -0
  62. package/rules/security/S047_oauth_pkce_protection/dart/analyzer.js +44 -0
  63. package/rules/security/S047_oauth_pkce_protection/index.js +86 -0
  64. package/rules/security/S047_oauth_pkce_protection/typescript/analyzer.js +78 -0
  65. package/rules/security/S048_oauth_redirect_uri_validation/config.json +30 -0
  66. package/rules/security/S048_oauth_redirect_uri_validation/typescript/analyzer.js +278 -0
  67. package/rules/security/S049_short_validity_tokens/typescript/config.json +10 -3
  68. package/rules/security/S050_reference_tokens_entropy/config.json +28 -0
  69. package/rules/security/S050_reference_tokens_entropy/dart/analyzer.js +45 -0
  70. package/rules/security/S050_reference_tokens_entropy/index.js +86 -0
  71. package/rules/security/S050_reference_tokens_entropy/typescript/analyzer.js +74 -0
  72. package/rules/security/S053_generic_error_messages/config.json +28 -0
  73. package/rules/security/S053_generic_error_messages/dart/analyzer.js +45 -0
  74. package/rules/security/S053_generic_error_messages/index.js +86 -0
  75. package/rules/security/S053_generic_error_messages/typescript/analyzer.js +80 -0
  76. package/rules/security/S055_content_type_validation/typescript/symbol-based-analyzer.js +64 -2
  77. package/rules/security/S059_disable_debug_mode/config.json +28 -0
  78. package/rules/security/S059_disable_debug_mode/dart/analyzer.js +45 -0
  79. package/rules/security/S059_disable_debug_mode/index.js +86 -0
  80. package/rules/security/S059_disable_debug_mode/typescript/analyzer.js +85 -0
  81. package/rules/security/S060_password_minimum_length/config.json +28 -0
  82. package/rules/security/S060_password_minimum_length/dart/analyzer.js +45 -0
  83. package/rules/security/S060_password_minimum_length/index.js +86 -0
  84. package/rules/security/S060_password_minimum_length/typescript/analyzer.js +78 -0
  85. package/rules/security/S026_json_schema_validation/config.json +0 -27
  86. package/rules/security/S026_json_schema_validation/typescript/analyzer.js +0 -251
  87. package/rules/security/S027_no_hardcoded_secrets/config.json +0 -29
  88. package/rules/security/S027_no_hardcoded_secrets/typescript/analyzer.js +0 -309
  89. package/rules/security/S027_no_hardcoded_secrets/typescript/categories.json +0 -153
  90. package/rules/security/S035_path_session_cookies/config.json +0 -99
  91. package/rules/security/S035_path_session_cookies/typescript/analyzer.js +0 -316
  92. package/rules/security/S035_path_session_cookies/typescript/regex-based-analyzer.js +0 -724
  93. package/rules/security/S035_path_session_cookies/typescript/symbol-based-analyzer.js +0 -373
  94. package/rules/security/S039_no_session_tokens_in_url/config.json +0 -92
  95. package/rules/security/S039_no_session_tokens_in_url/typescript/analyzer.js +0 -262
  96. package/rules/security/S039_no_session_tokens_in_url/typescript/regex-based-analyzer.js +0 -337
  97. package/rules/security/S039_no_session_tokens_in_url/typescript/symbol-based-analyzer.js +0 -443
  98. package/rules/security/S048_no_current_password_in_reset/config.json +0 -48
  99. package/rules/security/S048_no_current_password_in_reset/typescript/analyzer.js +0 -366
  100. /package/rules/security/{S026_json_schema_validation → S026_tls_all_connections}/dart/analyzer.js +0 -0
  101. /package/rules/security/{S026_json_schema_validation → S026_tls_all_connections}/index.js +0 -0
  102. /package/rules/security/{S027_no_hardcoded_secrets → S027_mtls_certificate_validation}/dart/analyzer.js +0 -0
  103. /package/rules/security/{S027_no_hardcoded_secrets → S027_mtls_certificate_validation}/index.js +0 -0
  104. /package/rules/security/{S027_no_hardcoded_secrets → S027_mtls_certificate_validation}/typescript/categorized-analyzer.js +0 -0
  105. /package/rules/security/{S035_path_session_cookies → S035_separate_app_hostnames}/dart/analyzer.js +0 -0
  106. /package/rules/security/{S035_path_session_cookies → S035_separate_app_hostnames}/index.js +0 -0
  107. /package/rules/security/{S035_path_session_cookies → S035_separate_app_hostnames}/typescript/README.md +0 -0
  108. /package/rules/security/{S039_no_session_tokens_in_url → S039_tls_certificate_validation}/dart/analyzer.js +0 -0
  109. /package/rules/security/{S039_no_session_tokens_in_url → S039_tls_certificate_validation}/index.js +0 -0
  110. /package/rules/security/{S039_no_session_tokens_in_url → S039_tls_certificate_validation}/typescript/README.md +0 -0
  111. /package/rules/security/{S048_no_current_password_in_reset → S048_oauth_redirect_uri_validation}/dart/analyzer.js +0 -0
  112. /package/rules/security/{S048_no_current_password_in_reset → S048_oauth_redirect_uri_validation}/index.js +0 -0
  113. /package/rules/security/{S048_no_current_password_in_reset → S048_oauth_redirect_uri_validation}/typescript/README.md +0 -0
@@ -1,11 +1,36 @@
1
+ /**
2
+ * S023 AST Analyzer – Use output encoding when building dynamic JavaScript/JSON
3
+ *
4
+ * AST-based detection for:
5
+ * - eval() with user data
6
+ * - new Function() with user data
7
+ * - String concatenation to build JavaScript code
8
+ * - Template literals with unescaped user input in JS/HTML context
9
+ * - Inline event handlers with user data
10
+ * - JSON.stringify in HTML context without proper escaping
11
+ */
12
+
1
13
  const fs = require('fs');
2
14
  const path = require('path');
3
15
 
4
16
  class S023ASTAnalyzer {
5
17
  constructor() {
6
18
  this.ruleId = 'S023';
7
- this.ruleName = 'No JSON Injection Prevention (AST-Enhanced)';
8
- this.description = 'AST-based detection of unsafe JSON parsing and injection vulnerabilities';
19
+ this.ruleName = 'Use output encoding when building dynamic JavaScript/JSON';
20
+ this.description = 'AST-based detection of unsafe dynamic JS/JSON building and injection vulnerabilities';
21
+
22
+ // User input source patterns
23
+ this.userInputSources = [
24
+ 'localStorage', 'sessionStorage', 'location', 'URLSearchParams',
25
+ 'req', 'request', 'document', 'window', 'fetch', 'axios',
26
+ 'getElementById', 'querySelector', 'formData', 'event'
27
+ ];
28
+
29
+ // Safe encoding functions
30
+ this.safeEncoders = [
31
+ 'encodeURIComponent', 'encodeURI', 'escape', 'escapeHtml',
32
+ 'sanitize', 'DOMPurify', 'textContent', 'htmlEncode', 'jsEncode'
33
+ ];
9
34
  }
10
35
 
11
36
  async analyze(files, language, options = {}) {
@@ -22,13 +47,12 @@ class S023ASTAnalyzer {
22
47
  ];
23
48
 
24
49
  for (const filePath of files) {
25
- // Skip test files
26
50
  if (skipPatterns.some(pattern => pattern.test(filePath))) {
27
51
  continue;
28
52
  }
29
53
 
30
54
  if (options.verbose) {
31
- console.log(`🎯 Running S023 AST analysis on ${path.basename(filePath)}`);
55
+ console.log(`🎯 S023 AST analysis on ${path.basename(filePath)}`);
32
56
  }
33
57
 
34
58
  try {
@@ -36,7 +60,9 @@ class S023ASTAnalyzer {
36
60
  const fileViolations = await this.analyzeFile(filePath, content, language, options);
37
61
  violations.push(...fileViolations);
38
62
  } catch (error) {
39
- console.warn(`⚠️ Failed to analyze ${filePath}: ${error.message}`);
63
+ if (options.verbose) {
64
+ console.warn(`⚠️ S023 AST: Failed to analyze ${filePath}: ${error.message}`);
65
+ }
40
66
  }
41
67
  }
42
68
 
@@ -55,30 +81,27 @@ class S023ASTAnalyzer {
55
81
 
56
82
  async analyzeJSTS(filePath, content, config) {
57
83
  const violations = [];
58
-
84
+
59
85
  try {
60
- // Try AST analysis first (like ESLint approach)
61
86
  const astViolations = await this.analyzeWithAST(filePath, content, config);
62
87
  if (astViolations.length > 0) {
63
88
  violations.push(...astViolations);
64
89
  }
65
90
  } catch (astError) {
66
91
  if (config.verbose) {
67
- console.log(`⚠️ AST analysis failed for ${path.basename(filePath)}, falling back to regex`);
92
+ console.log(`⚠️ S023 AST: AST parsing failed for ${path.basename(filePath)}, falling back to regex`);
68
93
  }
69
-
70
- // Fallback to regex-based analysis
94
+
71
95
  const regexViolations = await this.analyzeWithRegex(filePath, content, config);
72
96
  violations.push(...regexViolations);
73
97
  }
74
-
98
+
75
99
  return violations;
76
100
  }
77
101
 
78
102
  async analyzeWithAST(filePath, content, config) {
79
103
  const violations = [];
80
-
81
- // Import AST modules dynamically
104
+
82
105
  let astModules;
83
106
  try {
84
107
  astModules = require('../../../core/ast-modules');
@@ -86,7 +109,6 @@ class S023ASTAnalyzer {
86
109
  throw new Error('AST modules not available');
87
110
  }
88
111
 
89
- // Try to parse with AST
90
112
  let ast;
91
113
  try {
92
114
  ast = await astModules.parseCode(content, 'javascript', filePath);
@@ -97,31 +119,48 @@ class S023ASTAnalyzer {
97
119
  throw new Error(`Parse error: ${parseError.message}`);
98
120
  }
99
121
 
100
- // Traverse AST to find JSON injection vulnerabilities - mimicking ESLint's approach
101
122
  const rootNode = ast.program || ast;
102
123
  this.traverseAST(rootNode, (node) => {
103
- // Check JSON.parse() calls
124
+ // 1. Check eval() calls
125
+ if (this.isEvalCall(node)) {
126
+ const violation = this.checkEvalForUnsafeUsage(node, filePath, content);
127
+ if (violation) violations.push(violation);
128
+ }
129
+
130
+ // 2. Check new Function() calls
131
+ if (this.isNewFunctionCall(node)) {
132
+ const violation = this.checkNewFunctionForUnsafeUsage(node, filePath, content);
133
+ if (violation) violations.push(violation);
134
+ }
135
+
136
+ // 3. Check JSON.parse() calls
104
137
  if (this.isJsonParseCall(node)) {
105
138
  const violation = this.checkJsonParseForUnsafeUsage(node, filePath, content);
106
- if (violation) {
107
- violations.push(violation);
108
- }
139
+ if (violation) violations.push(violation);
109
140
  }
110
-
111
- // Check eval() with JSON patterns
112
- if (this.isEvalCall(node)) {
113
- const violation = this.checkEvalForJsonUsage(node, filePath, content);
114
- if (violation) {
115
- violations.push(violation);
116
- }
117
- }
118
-
119
- // Check JSON.stringify in HTML context
141
+
142
+ // 4. Check JSON.stringify in HTML context
120
143
  if (this.isJsonStringifyCall(node)) {
121
144
  const violation = this.checkJsonStringifyInHtmlContext(node, filePath, content);
122
- if (violation) {
123
- violations.push(violation);
124
- }
145
+ if (violation) violations.push(violation);
146
+ }
147
+
148
+ // 5. Check template literals in dangerous contexts
149
+ if (this.isTemplateLiteral(node)) {
150
+ const violation = this.checkTemplateLiteralForUnsafeUsage(node, filePath, content);
151
+ if (violation) violations.push(violation);
152
+ }
153
+
154
+ // 6. Check setAttribute for inline event handlers
155
+ if (this.isSetAttributeCall(node)) {
156
+ const violation = this.checkSetAttributeForEventHandler(node, filePath, content);
157
+ if (violation) violations.push(violation);
158
+ }
159
+
160
+ // 7. Check assignment to innerHTML/outerHTML with dynamic content
161
+ if (this.isAssignmentToHtmlProperty(node)) {
162
+ const violation = this.checkHtmlPropertyAssignment(node, filePath, content);
163
+ if (violation) violations.push(violation);
125
164
  }
126
165
  });
127
166
 
@@ -130,12 +169,12 @@ class S023ASTAnalyzer {
130
169
 
131
170
  traverseAST(node, callback) {
132
171
  if (!node || typeof node !== 'object') return;
133
-
172
+
134
173
  callback(node);
135
-
174
+
136
175
  for (const key in node) {
137
176
  if (key === 'parent' || key === 'leadingComments' || key === 'trailingComments') continue;
138
-
177
+
139
178
  const child = node[key];
140
179
  if (Array.isArray(child)) {
141
180
  child.forEach(item => this.traverseAST(item, callback));
@@ -145,228 +184,606 @@ class S023ASTAnalyzer {
145
184
  }
146
185
  }
147
186
 
187
+ // Node type checkers
188
+ isEvalCall(node) {
189
+ return node.type === 'CallExpression' &&
190
+ node.callee &&
191
+ node.callee.type === 'Identifier' &&
192
+ node.callee.name === 'eval';
193
+ }
194
+
195
+ isNewFunctionCall(node) {
196
+ return node.type === 'NewExpression' &&
197
+ node.callee &&
198
+ node.callee.type === 'Identifier' &&
199
+ node.callee.name === 'Function';
200
+ }
201
+
148
202
  isJsonParseCall(node) {
149
203
  return node.type === 'CallExpression' &&
150
- node.callee &&
204
+ node.callee &&
151
205
  node.callee.type === 'MemberExpression' &&
152
206
  node.callee.object && node.callee.object.name === 'JSON' &&
153
207
  node.callee.property && node.callee.property.name === 'parse';
154
208
  }
155
209
 
156
- isEvalCall(node) {
157
- return node.type === 'CallExpression' &&
158
- node.callee &&
159
- node.callee.type === 'Identifier' &&
160
- node.callee.name === 'eval';
161
- }
162
-
163
210
  isJsonStringifyCall(node) {
164
211
  return node.type === 'CallExpression' &&
165
- node.callee &&
212
+ node.callee &&
166
213
  node.callee.type === 'MemberExpression' &&
167
214
  node.callee.object && node.callee.object.name === 'JSON' &&
168
215
  node.callee.property && node.callee.property.name === 'stringify';
169
216
  }
170
217
 
218
+ isTemplateLiteral(node) {
219
+ return node.type === 'TemplateLiteral' &&
220
+ node.expressions &&
221
+ node.expressions.length > 0;
222
+ }
223
+
224
+ isSetAttributeCall(node) {
225
+ return node.type === 'CallExpression' &&
226
+ node.callee &&
227
+ node.callee.type === 'MemberExpression' &&
228
+ node.callee.property &&
229
+ node.callee.property.name === 'setAttribute';
230
+ }
231
+
232
+ isAssignmentToHtmlProperty(node) {
233
+ return node.type === 'AssignmentExpression' &&
234
+ node.left &&
235
+ node.left.type === 'MemberExpression' &&
236
+ node.left.property &&
237
+ (node.left.property.name === 'innerHTML' || node.left.property.name === 'outerHTML');
238
+ }
239
+
240
+ // Violation checkers
241
+ checkEvalForUnsafeUsage(node, filePath, content) {
242
+ if (!node.arguments || node.arguments.length === 0) return null;
243
+
244
+ const lines = content.split('\n');
245
+ const lineNumber = node.loc.start.line;
246
+ const lineText = lines[lineNumber - 1] || '';
247
+
248
+ const argument = node.arguments[0];
249
+
250
+ // Skip legitimate dynamic import pattern: eval(`import('module-name')`)
251
+ if (this.isDynamicImportPattern(argument, content)) {
252
+ return null;
253
+ }
254
+
255
+ if (this.containsUserInputOrDynamic(argument, content)) {
256
+ return {
257
+ ruleId: this.ruleId,
258
+ file: filePath,
259
+ line: lineNumber,
260
+ column: node.loc.start.column + 1,
261
+ message: 'Never use eval() with user data - use JSON.parse() for JSON or safer alternatives',
262
+ severity: 'error',
263
+ code: lineText.trim(),
264
+ type: 'eval_with_user_data',
265
+ confidence: 0.95,
266
+ suggestion: 'Use JSON.parse() for JSON data or refactor to avoid dynamic code execution'
267
+ };
268
+ }
269
+
270
+ return null;
271
+ }
272
+
273
+ /**
274
+ * Check if eval argument is a legitimate dynamic import pattern
275
+ * Pattern: eval(`import('module-name')`) - used for ESM imports in CommonJS
276
+ */
277
+ isDynamicImportPattern(node, content) {
278
+ if (!node) return false;
279
+
280
+ // Check for template literal containing only import()
281
+ if (node.type === 'TemplateLiteral') {
282
+ const nodeText = this.getNodeText(node, content);
283
+ // Match: `import('...')` or `import("...")`
284
+ return /^`\s*import\s*\(\s*['"][^'"]+['"]\s*\)\s*`$/.test(nodeText);
285
+ }
286
+
287
+ // Also check the text representation for regex fallback
288
+ const argText = this.getNodeText(node, content);
289
+ return /^`\s*import\s*\(\s*['"][^'"]+['"]\s*\)\s*`$/.test(argText);
290
+ }
291
+
292
+ checkNewFunctionForUnsafeUsage(node, filePath, content) {
293
+ if (!node.arguments || node.arguments.length === 0) return null;
294
+
295
+ const lines = content.split('\n');
296
+ const lineNumber = node.loc.start.line;
297
+ const lineText = lines[lineNumber - 1] || '';
298
+
299
+ // Check if any argument contains user input or dynamic data
300
+ const hasUnsafeArg = node.arguments.some(arg =>
301
+ this.containsUserInputOrDynamic(arg, content)
302
+ );
303
+
304
+ if (hasUnsafeArg) {
305
+ return {
306
+ ruleId: this.ruleId,
307
+ file: filePath,
308
+ line: lineNumber,
309
+ column: node.loc.start.column + 1,
310
+ message: 'Never use new Function() with user data - this is equivalent to eval()',
311
+ severity: 'error',
312
+ code: lineText.trim(),
313
+ type: 'new_function_with_user_data',
314
+ confidence: 0.95,
315
+ suggestion: 'Refactor to avoid dynamic function creation with user input'
316
+ };
317
+ }
318
+
319
+ return null;
320
+ }
321
+
171
322
  checkJsonParseForUnsafeUsage(node, filePath, content) {
172
323
  if (!node.arguments || node.arguments.length === 0) return null;
173
-
324
+
174
325
  const lines = content.split('\n');
175
326
  const lineNumber = node.loc.start.line;
176
327
  const lineText = lines[lineNumber - 1] || '';
177
-
178
- // Check if the argument is from user input (similar to ESLint logic)
328
+
179
329
  const argument = node.arguments[0];
180
330
  if (this.isUserInputSource(argument, content)) {
181
- // Check if there's validation before JSON.parse
182
331
  if (!this.hasValidationBefore(node, content)) {
183
332
  return {
184
333
  ruleId: this.ruleId,
185
334
  file: filePath,
186
335
  line: lineNumber,
187
336
  column: node.loc.start.column + 1,
188
- message: 'Unsafe JSON parsing - validate input before parsing',
337
+ message: 'Validate JSON structure before parsing user input',
189
338
  severity: 'warning',
190
339
  code: lineText.trim(),
191
340
  type: 'unsafe_json_parse',
192
341
  confidence: 0.8,
193
- suggestion: 'Validate input before parsing JSON'
342
+ suggestion: 'Wrap JSON.parse in try-catch and validate the parsed structure'
194
343
  };
195
344
  }
196
345
  }
197
-
346
+
198
347
  return null;
199
348
  }
200
349
 
201
- checkEvalForJsonUsage(node, filePath, content) {
202
- if (!node.arguments || node.arguments.length === 0) return null;
203
-
350
+ checkJsonStringifyInHtmlContext(node, filePath, content) {
204
351
  const lines = content.split('\n');
205
352
  const lineNumber = node.loc.start.line;
206
353
  const lineText = lines[lineNumber - 1] || '';
207
-
208
- // Check if eval contains JSON patterns
209
- const argument = node.arguments[0];
210
- if (this.containsJsonPattern(argument, content)) {
211
- return {
212
- ruleId: this.ruleId,
213
- file: filePath,
214
- line: lineNumber,
215
- column: node.loc.start.column + 1,
216
- message: 'Never use eval() to process JSON data - use JSON.parse() instead',
217
- severity: 'error',
218
- code: lineText.trim(),
219
- type: 'eval_json',
220
- confidence: 0.9,
221
- suggestion: 'Use JSON.parse() instead of eval()'
222
- };
354
+
355
+ if (this.isInHtmlContext(node, content)) {
356
+ const surroundingText = this.getSurroundingText(node, content, 3);
357
+
358
+ if (/<script|<\/script>/i.test(surroundingText)) {
359
+ const hasEscaping = /replace\s*\(\s*[/']<\\?\/script/i.test(surroundingText);
360
+
361
+ if (!hasEscaping) {
362
+ return {
363
+ ruleId: this.ruleId,
364
+ file: filePath,
365
+ line: lineNumber,
366
+ column: node.loc.start.column + 1,
367
+ message: 'JSON.stringify in HTML/script context must escape </script> sequences',
368
+ severity: 'warning',
369
+ code: lineText.trim(),
370
+ type: 'json_stringify_html_no_escape',
371
+ confidence: 0.8,
372
+ suggestion: "Escape </script> with .replace(/<\\/script/gi, '<\\\\/script')"
373
+ };
374
+ }
375
+ }
223
376
  }
224
-
377
+
225
378
  return null;
226
379
  }
227
380
 
228
- checkJsonStringifyInHtmlContext(node, filePath, content) {
381
+ checkTemplateLiteralForUnsafeUsage(node, filePath, content) {
229
382
  const lines = content.split('\n');
230
383
  const lineNumber = node.loc.start.line;
231
384
  const lineText = lines[lineNumber - 1] || '';
232
-
233
- // Check if JSON.stringify is used in HTML context
234
- if (this.isInHtmlContext(node, content)) {
235
- return {
236
- ruleId: this.ruleId,
237
- file: filePath,
238
- line: lineNumber,
239
- column: node.loc.start.column + 1,
240
- message: 'JSON.stringify output should be escaped when used in HTML context',
241
- severity: 'warning',
242
- code: lineText.trim(),
243
- type: 'json_stringify_html',
244
- confidence: 0.7,
245
- suggestion: 'Escape JSON.stringify output in HTML context'
246
- };
385
+
386
+ // Check if template literal is in dangerous context
387
+ const surroundingText = this.getSurroundingText(node, content, 2);
388
+
389
+ const isDangerousContext =
390
+ /innerHTML\s*=/.test(surroundingText) ||
391
+ /outerHTML\s*=/.test(surroundingText) ||
392
+ /insertAdjacentHTML/.test(surroundingText) ||
393
+ /document\.write/.test(surroundingText);
394
+
395
+ if (isDangerousContext) {
396
+ // Check if any expression contains user input
397
+ const hasUserInputExpression = node.expressions.some(expr =>
398
+ this.containsUserInputOrDynamic(expr, content)
399
+ );
400
+
401
+ if (hasUserInputExpression && !this.hasSafeEncoding(lineText)) {
402
+ return {
403
+ ruleId: this.ruleId,
404
+ file: filePath,
405
+ line: lineNumber,
406
+ column: node.loc.start.column + 1,
407
+ message: 'Template literal with user input in dangerous context - escape data before insertion',
408
+ severity: 'warning',
409
+ code: lineText.trim(),
410
+ type: 'unsafe_template_literal',
411
+ confidence: 0.75,
412
+ suggestion: 'Use textContent for text, or properly escape data before inserting into HTML context'
413
+ };
414
+ }
247
415
  }
248
-
416
+
249
417
  return null;
250
418
  }
251
419
 
252
- isUserInputSource(node, content) {
420
+ checkSetAttributeForEventHandler(node, filePath, content) {
421
+ if (!node.arguments || node.arguments.length < 2) return null;
422
+
423
+ const lines = content.split('\n');
424
+ const lineNumber = node.loc.start.line;
425
+ const lineText = lines[lineNumber - 1] || '';
426
+
427
+ const firstArg = node.arguments[0];
428
+ const attrName = this.getStringValue(firstArg);
429
+
430
+ // Check if it's an event handler attribute
431
+ if (attrName && /^on\w+$/i.test(attrName)) {
432
+ const valueArg = node.arguments[1];
433
+
434
+ if (this.containsUserInputOrDynamic(valueArg, content)) {
435
+ return {
436
+ ruleId: this.ruleId,
437
+ file: filePath,
438
+ line: lineNumber,
439
+ column: node.loc.start.column + 1,
440
+ message: 'Avoid inline event handlers with user data - use addEventListener instead',
441
+ severity: 'warning',
442
+ code: lineText.trim(),
443
+ type: 'inline_event_handler_user_data',
444
+ confidence: 0.8,
445
+ suggestion: 'Use element.addEventListener() and pass data via data attributes or closures'
446
+ };
447
+ }
448
+ }
449
+
450
+ return null;
451
+ }
452
+
453
+ checkHtmlPropertyAssignment(node, filePath, content) {
454
+ const lines = content.split('\n');
455
+ const lineNumber = node.loc.start.line;
456
+ const lineText = lines[lineNumber - 1] || '';
457
+
458
+ const rightSide = node.right;
459
+
460
+ // Check if right side contains user input without encoding
461
+ if (this.containsUserInputOrDynamic(rightSide, content)) {
462
+ if (!this.hasSafeEncoding(lineText)) {
463
+ return {
464
+ ruleId: this.ruleId,
465
+ file: filePath,
466
+ line: lineNumber,
467
+ column: node.loc.start.column + 1,
468
+ message: 'Assignment to innerHTML/outerHTML with dynamic content - escape user input',
469
+ severity: 'warning',
470
+ code: lineText.trim(),
471
+ type: 'unsafe_html_assignment',
472
+ confidence: 0.75,
473
+ suggestion: 'Use textContent for text or properly escape/sanitize HTML content'
474
+ };
475
+ }
476
+ }
477
+
478
+ return null;
479
+ }
480
+
481
+ // Helper methods
482
+ containsUserInputOrDynamic(node, content) {
253
483
  if (!node) return false;
254
-
255
- // Check for common user input patterns (similar to ESLint)
256
- const userInputPatterns = [
257
- /localStorage\.getItem/,
258
- /sessionStorage\.getItem/,
259
- /window\.location/,
260
- /location\.(search|hash)/,
261
- /URLSearchParams/,
262
- /req\.(body|query|params)/,
263
- /request\.(body|query|params)/
264
- ];
265
-
266
- return userInputPatterns.some(pattern => {
267
- const nodeText = this.getNodeText(node, content);
268
- return pattern.test(nodeText);
269
- });
484
+
485
+ const nodeText = this.getNodeText(node, content);
486
+
487
+ // Check for user input sources
488
+ const isUserInput = this.userInputSources.some(source =>
489
+ nodeText.includes(source)
490
+ );
491
+
492
+ // Check for dynamic patterns (concatenation, template literals)
493
+ const isDynamic =
494
+ node.type === 'BinaryExpression' ||
495
+ node.type === 'TemplateLiteral' ||
496
+ /\+/.test(nodeText) ||
497
+ /\$\{/.test(nodeText);
498
+
499
+ return isUserInput || isDynamic;
500
+ }
501
+
502
+ isUserInputSource(node, content) {
503
+ const nodeText = this.getNodeText(node, content);
504
+ return this.userInputSources.some(source => nodeText.includes(source));
270
505
  }
271
506
 
272
507
  hasValidationBefore(node, content) {
273
- // Simple check for validation patterns before JSON.parse
274
508
  const lines = content.split('\n');
275
509
  const lineNumber = node.loc.start.line;
276
-
510
+
277
511
  // Check previous lines for validation patterns
278
512
  for (let i = Math.max(0, lineNumber - 5); i < lineNumber - 1; i++) {
279
513
  const line = lines[i] || '';
280
- if (this.containsValidationPattern(line)) {
514
+ if (/try\s*\{|catch\s*\(|if\s*\(|typeof\s+|validate|check|isValid/i.test(line)) {
281
515
  return true;
282
516
  }
283
517
  }
284
-
518
+
285
519
  return false;
286
520
  }
287
521
 
288
- containsValidationPattern(line) {
289
- const validationPatterns = [
290
- /try\s*{/,
291
- /catch\s*\(/,
292
- /if\s*\(/,
293
- /typeof\s+/,
294
- /instanceof\s+/,
295
- /\.length\s*>/,
296
- /validate/i,
297
- /check/i,
298
- /isValid/i
299
- ];
300
-
301
- return validationPatterns.some(pattern => pattern.test(line));
302
- }
303
-
304
- containsJsonPattern(node, content) {
305
- const nodeText = this.getNodeText(node, content);
306
- return /json|JSON|\{|\[/.test(nodeText);
522
+ hasSafeEncoding(text) {
523
+ return this.safeEncoders.some(encoder =>
524
+ text.toLowerCase().includes(encoder.toLowerCase())
525
+ );
307
526
  }
308
527
 
309
528
  isInHtmlContext(node, content) {
310
- // Check if JSON.stringify is used in HTML context
311
- const htmlPatterns = [
312
- /innerHTML/,
313
- /outerHTML/,
314
- /insertAdjacentHTML/,
315
- /document\.write/,
316
- /\.html\(/
317
- ];
318
-
319
529
  const surroundingText = this.getSurroundingText(node, content, 3);
320
- return htmlPatterns.some(pattern => pattern.test(surroundingText));
530
+ return /innerHTML|outerHTML|insertAdjacentHTML|document\.write|<script|<\/script>/i.test(surroundingText);
321
531
  }
322
532
 
323
533
  getNodeText(node, content) {
324
534
  if (!node || !node.loc) return '';
325
- const lines = content.split('\n');
326
- return lines[node.loc.start.line - 1] || '';
535
+ try {
536
+ const lines = content.split('\n');
537
+ const startLine = node.loc.start.line - 1;
538
+ const endLine = node.loc.end.line - 1;
539
+
540
+ if (startLine === endLine) {
541
+ return lines[startLine].substring(node.loc.start.column, node.loc.end.column);
542
+ }
543
+
544
+ return lines.slice(startLine, endLine + 1).join('\n');
545
+ } catch (e) {
546
+ return '';
547
+ }
327
548
  }
328
549
 
329
550
  getSurroundingText(node, content, radius = 2) {
330
551
  if (!node || !node.loc) return '';
331
552
  const lines = content.split('\n');
332
553
  const lineNumber = node.loc.start.line;
333
-
554
+
334
555
  const startLine = Math.max(0, lineNumber - radius - 1);
335
556
  const endLine = Math.min(lines.length, lineNumber + radius);
336
-
557
+
337
558
  return lines.slice(startLine, endLine).join('\n');
338
559
  }
339
560
 
561
+ getStringValue(node) {
562
+ if (!node) return null;
563
+ if (node.type === 'Literal' && typeof node.value === 'string') {
564
+ return node.value;
565
+ }
566
+ if (node.type === 'StringLiteral') {
567
+ return node.value;
568
+ }
569
+ return null;
570
+ }
571
+
340
572
  async analyzeWithRegex(filePath, content, config) {
341
- // Fallback regex analysis for basic JSON.parse detection
342
573
  const violations = [];
343
574
  const lines = content.split('\n');
344
-
345
- const jsonParsePattern = /JSON\.parse\s*\(\s*([^)]+)\)/g;
346
575
  let match;
347
-
576
+
577
+ // User input patterns for detection
578
+ const userInputPattern = /localStorage|sessionStorage|location|req\.|request\.|\.value|getElementById|querySelector|URLSearchParams|formData|event\.data/i;
579
+
580
+ // 1. Check for eval with dynamic content
581
+ // Use a more flexible pattern that can capture multiline arguments
582
+ const evalPattern = /\beval\s*\(\s*(`[\s\S]*?`|[^)]+)\)/g;
583
+
584
+ // Pattern for legitimate dynamic imports: eval(`import('...')`) or eval(`import("...")`)
585
+ // Allows whitespace/newlines between tokens
586
+ const dynamicImportPattern = /^`\s*import\s*\(\s*['"][^'"]+['"]\s*\)\s*`$/s;
587
+
588
+ while ((match = evalPattern.exec(content)) !== null) {
589
+ const line = content.substring(0, match.index).split('\n').length;
590
+ const lineText = lines[line - 1] || '';
591
+ const argument = match[1].trim().replace(/\s+/g, ' '); // Normalize whitespace
592
+
593
+ // Skip legitimate dynamic import pattern: eval(`import('module-name')`)
594
+ // Also normalize argument for checking
595
+ const normalizedArg = argument.replace(/\s+/g, '');
596
+ if (/^`import\(['"][^'"]+['"]\)`$/.test(normalizedArg)) {
597
+ continue;
598
+ }
599
+
600
+ if (/\+|`|\$\{/.test(argument) || userInputPattern.test(argument)) {
601
+ violations.push({
602
+ ruleId: this.ruleId,
603
+ file: filePath,
604
+ line: line,
605
+ column: match.index - content.lastIndexOf('\n', match.index),
606
+ message: 'Never use eval() with user data - use JSON.parse() for JSON',
607
+ severity: 'error',
608
+ code: lineText.trim(),
609
+ type: 'eval_with_user_data_regex',
610
+ confidence: 0.8,
611
+ suggestion: 'Use JSON.parse() or refactor to avoid dynamic code execution'
612
+ });
613
+ }
614
+ }
615
+
616
+ // 2. Check for new Function with dynamic content
617
+ const newFunctionPattern = /new\s+Function\s*\(\s*([^)]*)\)/g;
618
+
619
+ while ((match = newFunctionPattern.exec(content)) !== null) {
620
+ const line = content.substring(0, match.index).split('\n').length;
621
+ const lineText = lines[line - 1] || '';
622
+ const argument = match[1];
623
+
624
+ if (/\+|`|\$\{/.test(argument) || userInputPattern.test(argument)) {
625
+ violations.push({
626
+ ruleId: this.ruleId,
627
+ file: filePath,
628
+ line: line,
629
+ column: match.index - content.lastIndexOf('\n', match.index),
630
+ message: 'Never use new Function() with user data - equivalent to eval()',
631
+ severity: 'error',
632
+ code: lineText.trim(),
633
+ type: 'new_function_with_user_data_regex',
634
+ confidence: 0.8,
635
+ suggestion: 'Refactor to avoid dynamic function creation'
636
+ });
637
+ }
638
+ }
639
+
640
+ // 3. Check for innerHTML/outerHTML with template literals or concatenation
641
+ const htmlAssignmentPattern = /\.(innerHTML|outerHTML)\s*=\s*(`[^`]*\$\{|[^;]*\+)/g;
642
+
643
+ while ((match = htmlAssignmentPattern.exec(content)) !== null) {
644
+ const line = content.substring(0, match.index).split('\n').length;
645
+ const lineText = lines[line - 1] || '';
646
+
647
+ // Check if there's encoding/sanitization
648
+ if (!this.hasSafeEncoding(lineText)) {
649
+ violations.push({
650
+ ruleId: this.ruleId,
651
+ file: filePath,
652
+ line: line,
653
+ column: match.index - content.lastIndexOf('\n', match.index),
654
+ message: 'Avoid innerHTML/outerHTML with dynamic content - use textContent or sanitize input',
655
+ severity: 'warning',
656
+ code: lineText.trim(),
657
+ type: 'unsafe_html_assignment_regex',
658
+ confidence: 0.75,
659
+ suggestion: 'Use textContent for text, or DOMPurify/escapeHtml for HTML content'
660
+ });
661
+ }
662
+ }
663
+
664
+ // 4. Check for setAttribute with event handlers
665
+ const setAttributePattern = /setAttribute\s*\(\s*['"]on\w+['"]\s*,\s*([^)]+)\)/gi;
666
+
667
+ while ((match = setAttributePattern.exec(content)) !== null) {
668
+ const line = content.substring(0, match.index).split('\n').length;
669
+ const lineText = lines[line - 1] || '';
670
+ const value = match[1];
671
+
672
+ if (/\+|`|\$\{/.test(value) || userInputPattern.test(value)) {
673
+ violations.push({
674
+ ruleId: this.ruleId,
675
+ file: filePath,
676
+ line: line,
677
+ column: match.index - content.lastIndexOf('\n', match.index),
678
+ message: 'Avoid inline event handlers with user data - use addEventListener instead',
679
+ severity: 'warning',
680
+ code: lineText.trim(),
681
+ type: 'inline_event_handler_regex',
682
+ confidence: 0.8,
683
+ suggestion: 'Use element.addEventListener() and pass data via data attributes'
684
+ });
685
+ }
686
+ }
687
+
688
+ // 5. Check for JSON.stringify in script tag context without escaping
689
+ const jsonStringifyInScriptPattern = /<script[^>]*>.*JSON\.stringify\s*\([^)]+\).*<\/script>/gi;
690
+
691
+ while ((match = jsonStringifyInScriptPattern.exec(content)) !== null) {
692
+ const line = content.substring(0, match.index).split('\n').length;
693
+ const lineText = lines[line - 1] || '';
694
+ const matchText = match[0];
695
+
696
+ // Check if </script is properly escaped
697
+ if (!(/replace\s*\(\s*[/']<\\?\/script/i.test(matchText))) {
698
+ violations.push({
699
+ ruleId: this.ruleId,
700
+ file: filePath,
701
+ line: line,
702
+ column: match.index - content.lastIndexOf('\n', match.index),
703
+ message: 'JSON.stringify in script context must escape </script> sequences',
704
+ severity: 'warning',
705
+ code: lineText.trim(),
706
+ type: 'json_stringify_script_no_escape_regex',
707
+ confidence: 0.8,
708
+ suggestion: "Escape </script> with .replace(/<\\/script/gi, '<\\\\/script')"
709
+ });
710
+ }
711
+ }
712
+
713
+ // 6. Check for JSON.parse with user input without try-catch
714
+ const jsonParsePattern = /JSON\.parse\s*\(\s*([^)]+)\)/g;
715
+
348
716
  while ((match = jsonParsePattern.exec(content)) !== null) {
349
717
  const line = content.substring(0, match.index).split('\n').length;
350
718
  const lineText = lines[line - 1] || '';
351
-
352
- // Simple check for unsafe patterns
353
719
  const argument = match[1];
354
- if (/localStorage\.getItem|sessionStorage\.getItem/.test(argument)) {
720
+
721
+ if (userInputPattern.test(argument)) {
722
+ // Check if there's a try-catch around it
723
+ const contextStart = Math.max(0, match.index - 200);
724
+ const contextBefore = content.substring(contextStart, match.index);
725
+
726
+ if (!/try\s*\{[^}]*$/.test(contextBefore)) {
727
+ violations.push({
728
+ ruleId: this.ruleId,
729
+ file: filePath,
730
+ line: line,
731
+ column: match.index - content.lastIndexOf('\n', match.index),
732
+ message: 'Validate JSON structure before parsing user input',
733
+ severity: 'warning',
734
+ code: lineText.trim(),
735
+ type: 'unsafe_json_parse_regex',
736
+ confidence: 0.7,
737
+ suggestion: 'Wrap JSON.parse in try-catch and validate the parsed structure'
738
+ });
739
+ }
740
+ }
741
+ }
742
+
743
+ // 7. Check for document.write with dynamic content
744
+ const documentWritePattern = /document\.write(ln)?\s*\(\s*(`[^`]*\$\{|[^)]*\+)/g;
745
+
746
+ while ((match = documentWritePattern.exec(content)) !== null) {
747
+ const line = content.substring(0, match.index).split('\n').length;
748
+ const lineText = lines[line - 1] || '';
749
+
750
+ violations.push({
751
+ ruleId: this.ruleId,
752
+ file: filePath,
753
+ line: line,
754
+ column: match.index - content.lastIndexOf('\n', match.index),
755
+ message: 'Avoid document.write with dynamic content - use DOM methods instead',
756
+ severity: 'warning',
757
+ code: lineText.trim(),
758
+ type: 'unsafe_document_write_regex',
759
+ confidence: 0.75,
760
+ suggestion: 'Use createElement and textContent for safer DOM manipulation'
761
+ });
762
+ }
763
+
764
+ // 8. Check for insertAdjacentHTML with dynamic content
765
+ const insertAdjacentPattern = /insertAdjacentHTML\s*\(\s*['"][^'"]+['"]\s*,\s*(`[^`]*\$\{|[^)]*\+)/g;
766
+
767
+ while ((match = insertAdjacentPattern.exec(content)) !== null) {
768
+ const line = content.substring(0, match.index).split('\n').length;
769
+ const lineText = lines[line - 1] || '';
770
+
771
+ if (!this.hasSafeEncoding(lineText)) {
355
772
  violations.push({
356
773
  ruleId: this.ruleId,
357
774
  file: filePath,
358
775
  line: line,
359
776
  column: match.index - content.lastIndexOf('\n', match.index),
360
- message: 'Unsafe JSON parsing - validate input before parsing',
777
+ message: 'insertAdjacentHTML with dynamic content - sanitize input first',
361
778
  severity: 'warning',
362
779
  code: lineText.trim(),
363
- type: 'unsafe_json_parse_regex',
364
- confidence: 0.6,
365
- suggestion: 'Validate input before parsing JSON'
780
+ type: 'unsafe_insert_adjacent_regex',
781
+ confidence: 0.75,
782
+ suggestion: 'Use DOMPurify or escape user input before inserting HTML'
366
783
  });
367
784
  }
368
785
  }
369
-
786
+
370
787
  return violations;
371
788
  }
372
789
  }