@sun-asterisk/sunlint 1.3.16 → 1.3.17

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 (50) hide show
  1. package/config/rule-analysis-strategies.js +3 -3
  2. package/config/rules/enhanced-rules-registry.json +40 -20
  3. package/core/cli-action-handler.js +2 -2
  4. package/core/config-merger.js +28 -6
  5. package/core/constants/defaults.js +1 -1
  6. package/core/file-targeting-service.js +72 -4
  7. package/core/output-service.js +21 -4
  8. package/engines/heuristic-engine.js +5 -0
  9. package/package.json +1 -1
  10. package/rules/common/C002_no_duplicate_code/README.md +115 -0
  11. package/rules/common/C002_no_duplicate_code/analyzer.js +615 -219
  12. package/rules/common/C002_no_duplicate_code/test-cases/api-handlers.ts +64 -0
  13. package/rules/common/C002_no_duplicate_code/test-cases/data-processor.ts +46 -0
  14. package/rules/common/C002_no_duplicate_code/test-cases/good-example.tsx +40 -0
  15. package/rules/common/C002_no_duplicate_code/test-cases/product-service.ts +57 -0
  16. package/rules/common/C002_no_duplicate_code/test-cases/user-service.ts +49 -0
  17. package/rules/common/C008/analyzer.js +40 -0
  18. package/rules/common/C008/config.json +20 -0
  19. package/rules/common/C008/ts-morph-analyzer.js +1067 -0
  20. package/rules/common/C018_no_throw_generic_error/analyzer.js +1 -1
  21. package/rules/common/C018_no_throw_generic_error/symbol-based-analyzer.js +27 -3
  22. package/rules/common/C024_no_scatter_hardcoded_constants/symbol-based-analyzer.js +504 -162
  23. package/rules/common/C029_catch_block_logging/analyzer.js +499 -89
  24. package/rules/common/C033_separate_service_repository/README.md +131 -20
  25. package/rules/common/C033_separate_service_repository/analyzer.js +1 -1
  26. package/rules/common/C033_separate_service_repository/symbol-based-analyzer.js +417 -274
  27. package/rules/common/C041_no_sensitive_hardcode/analyzer.js +144 -254
  28. package/rules/common/C041_no_sensitive_hardcode/config.json +50 -0
  29. package/rules/common/C041_no_sensitive_hardcode/symbol-based-analyzer.js +575 -0
  30. package/rules/common/C067_no_hardcoded_config/analyzer.js +17 -16
  31. package/rules/common/C067_no_hardcoded_config/symbol-based-analyzer.js +3477 -659
  32. package/rules/docs/C002_no_duplicate_code.md +276 -11
  33. package/rules/index.js +5 -1
  34. package/rules/security/S006_no_plaintext_recovery_codes/analyzer.js +266 -88
  35. package/rules/security/S006_no_plaintext_recovery_codes/symbol-based-analyzer.js +805 -0
  36. package/rules/security/S010_no_insecure_encryption/README.md +78 -0
  37. package/rules/security/S010_no_insecure_encryption/analyzer.js +463 -398
  38. package/rules/security/S013_tls_enforcement/README.md +51 -0
  39. package/rules/security/S013_tls_enforcement/analyzer.js +99 -0
  40. package/rules/security/S013_tls_enforcement/config.json +41 -0
  41. package/rules/security/S013_tls_enforcement/symbol-based-analyzer.js +339 -0
  42. package/rules/security/S014_tls_version_enforcement/README.md +354 -0
  43. package/rules/security/S014_tls_version_enforcement/analyzer.js +118 -0
  44. package/rules/security/S014_tls_version_enforcement/config.json +56 -0
  45. package/rules/security/S014_tls_version_enforcement/symbol-based-analyzer.js +194 -0
  46. package/rules/security/S055_content_type_validation/analyzer.js +121 -279
  47. package/rules/security/S055_content_type_validation/symbol-based-analyzer.js +346 -0
  48. package/rules/tests/C002_no_duplicate_code.test.js +111 -22
  49. package/rules/common/C029_catch_block_logging/analyzer-smart-pipeline.js +0 -755
  50. package/rules/common/C041_no_sensitive_hardcode/ast-analyzer.js +0 -296
@@ -1,87 +1,241 @@
1
1
  /**
2
- * Heuristic analyzer for S006 - No Plaintext Recovery/Activation Codes
3
- * Purpose: Detect sending recovery codes, activation codes, or reset codes in plaintext
2
+ * S006 - No Plaintext Recovery/Activation Codes
3
+ *
4
+ * Main analyzer using symbol-based analysis to detect plaintext recovery/activation codes
4
5
  * Based on OWASP A02:2021 - Cryptographic Failures
5
6
  */
6
7
 
8
+ // Command: node cli.js --rule=S006 --input=examples/rule-test-fixtures/rules/S006_no_plaintext_recovery_codes --engine=heuristic
9
+
10
+ const S006SymbolBasedAnalyzer = require("./symbol-based-analyzer");
11
+
7
12
  class S006Analyzer {
13
+ constructor(options = {}) {
14
+ this.ruleId = "S006";
15
+ this.semanticEngine = options.semanticEngine || null;
16
+ this.verbose = options.verbose || false;
17
+
18
+ try {
19
+ this.symbolAnalyzer = new S006SymbolBasedAnalyzer(this.semanticEngine);
20
+ } catch (e) {
21
+ console.warn(`⚠ [S006] Failed to create symbol analyzer: ${e.message}`);
22
+ }
23
+ }
24
+
25
+ async initialize(semanticEngine) {
26
+ this.semanticEngine = semanticEngine;
27
+ if (this.symbolAnalyzer && this.symbolAnalyzer.initialize) {
28
+ await this.symbolAnalyzer.initialize(semanticEngine);
29
+ }
30
+ }
31
+
32
+ analyzeSingle(filePath, options = {}) {
33
+ return this.analyze([filePath], "typescript", options);
34
+ }
35
+
36
+ async analyze(files, language, options = {}) {
37
+ const violations = [];
38
+ for (const filePath of files) {
39
+ try {
40
+ const vs = await this.analyzeFile(filePath, options);
41
+ violations.push(...vs);
42
+ } catch (e) {
43
+ console.warn(`⚠ [S006] Analysis error for ${filePath}: ${e.message}`);
44
+ }
45
+ }
46
+ return violations;
47
+ }
48
+
49
+ async analyzeFile(filePath, options = {}) {
50
+ const violationMap = new Map();
51
+
52
+ if (!this.symbolAnalyzer) {
53
+ return [];
54
+ }
55
+
56
+ // Skip test files, build directories, and node_modules
57
+ if (this.shouldSkipFile(filePath)) {
58
+ return [];
59
+ }
60
+
61
+ try {
62
+ let sourceFile = null;
63
+ if (this.semanticEngine?.project) {
64
+ sourceFile = this.semanticEngine.project.getSourceFile(filePath);
65
+ }
66
+
67
+ if (!sourceFile) {
68
+ // Create temporary ts-morph source file
69
+ const fs = require("fs");
70
+ const path = require("path");
71
+ const { Project } = require("ts-morph");
72
+ if (!fs.existsSync(filePath)) {
73
+ throw new Error(`File not found: ${filePath}`);
74
+ }
75
+ const content = fs.readFileSync(filePath, "utf8");
76
+ const tmp = new Project({
77
+ useInMemoryFileSystem: true,
78
+ compilerOptions: { allowJs: true },
79
+ });
80
+ sourceFile = tmp.createSourceFile(path.basename(filePath), content);
81
+ }
82
+
83
+ if (sourceFile) {
84
+ const symbolViolations = await this.symbolAnalyzer.analyze(
85
+ sourceFile,
86
+ filePath
87
+ );
88
+ symbolViolations.forEach((v) => {
89
+ const key = `${v.line}:${v.column}:${v.message}`;
90
+ if (!violationMap.has(key)) violationMap.set(key, v);
91
+ });
92
+ }
93
+ } catch (e) {
94
+ console.warn(`⚠ [S006] Symbol analysis failed: ${e.message}`);
95
+ }
96
+
97
+ return Array.from(violationMap.values()).map((v) => ({
98
+ ...v,
99
+ filePath,
100
+ file: filePath,
101
+ }));
102
+ }
103
+
104
+ shouldSkipFile(filePath) {
105
+ const skipPatterns = [
106
+ "test/",
107
+ "tests/",
108
+ "__tests__/",
109
+ ".test.",
110
+ ".spec.",
111
+ "node_modules/",
112
+ "build/",
113
+ "dist/",
114
+ ".next/",
115
+ "coverage/",
116
+ "vendor/",
117
+ "mocks/",
118
+ ".mock.",
119
+ ];
120
+
121
+ return skipPatterns.some((pattern) => filePath.includes(pattern));
122
+ }
123
+
124
+ cleanup() {
125
+ if (this.symbolAnalyzer?.cleanup) {
126
+ this.symbolAnalyzer.cleanup();
127
+ }
128
+ }
129
+ }
130
+
131
+ // Keep old implementation for reference but not used
132
+ class S006AnalyzerOld {
8
133
  constructor() {
9
- this.ruleId = 'S006';
10
- this.ruleName = 'No Plaintext Recovery/Activation Codes';
11
- this.description = 'Do not send recovery or activation codes in plaintext';
12
-
134
+ this.ruleId = "S006";
135
+ this.ruleName = "No Plaintext Recovery/Activation Codes";
136
+ this.description = "Do not send recovery or activation codes in plaintext";
137
+
13
138
  // Keywords that indicate sensitive codes
14
139
  this.sensitiveCodeKeywords = [
15
- 'recovery', 'activation', 'reset', 'verification', 'confirm', 'verify',
16
- 'otp', 'totp', 'code', 'pin', 'token', 'secret', 'key', 'password'
140
+ "recovery",
141
+ "activation",
142
+ "reset",
143
+ "verification",
144
+ "confirm",
145
+ "verify",
146
+ "otp",
147
+ "totp",
148
+ "code",
149
+ "pin",
150
+ "token",
151
+ "secret",
152
+ "key",
153
+ "password",
17
154
  ];
18
-
155
+
19
156
  // Keywords that indicate code sending/transmission
20
157
  this.sendingKeywords = [
21
- 'send', 'email', 'sms', 'text', 'message', 'mail', 'push', 'notify',
22
- 'transmit', 'deliver', 'dispatch', 'forward', 'post', 'put', 'create',
23
- 'response', 'body', 'content', 'payload', 'data'
158
+ "send",
159
+ "email",
160
+ "sms",
161
+ "text",
162
+ "message",
163
+ "mail",
164
+ "push",
165
+ "notify",
166
+ "transmit",
167
+ "deliver",
168
+ "dispatch",
169
+ "forward",
170
+ "post",
171
+ "put",
172
+ "create",
173
+ "response",
174
+ "body",
175
+ "content",
176
+ "payload",
177
+ "data",
24
178
  ];
25
-
179
+
26
180
  // Patterns that indicate plaintext transmission
27
181
  this.plaintextPatterns = [
28
182
  // Email/SMS sending with codes
29
183
  /(?:send|email|sms|text|message).*(?:recovery|activation|reset|verification|otp|code|pin)/i,
30
184
  /(?:recovery|activation|reset|verification|otp|code|pin).*(?:send|email|sms|text|message)/i,
31
-
185
+
32
186
  // HTTP responses with codes in body
33
187
  /(?:response|body|json|data|payload).*(?:recovery|activation|reset|verification|otp|code|pin)/i,
34
188
  /(?:recovery|activation|reset|verification|otp|code|pin).*(?:response|body|json|data|payload)/i,
35
-
189
+
36
190
  // Direct code exposure in strings
37
191
  /".*(?:recovery|activation|reset|verification|otp|code|pin).*"/i,
38
192
  /'.*(?:recovery|activation|reset|verification|otp|code|pin).*'/i,
39
193
  /`.*(?:recovery|activation|reset|verification|otp|code|pin).*`/i,
40
-
194
+
41
195
  // Template strings with codes
42
196
  /\$\{.*(?:recovery|activation|reset|verification|otp|code|pin).*\}/i,
43
-
197
+
44
198
  // API endpoint responses
45
199
  /return.*(?:recovery|activation|reset|verification|otp|code|pin)/i,
46
200
  /res\.(?:send|json|end).*(?:recovery|activation|reset|verification|otp|code|pin)/i,
47
201
  ];
48
-
202
+
49
203
  // Patterns that should be excluded (safe practices)
50
204
  this.safePatterns = [
51
205
  // Hashed or encrypted codes
52
206
  /hash|encrypt|cipher|bcrypt|crypto|secure/i,
53
-
207
+
54
208
  // Environment variables or config
55
209
  /process\.env|config\.|getenv/i,
56
-
210
+
57
211
  // Database storage (not transmission)
58
212
  /save|store|insert|update|database|db\./i,
59
-
213
+
60
214
  // Logging patterns (depends on context but often acceptable for debugging)
61
215
  /log|debug|trace|console/i,
62
-
216
+
63
217
  // Comments and documentation
64
218
  /\/\/|\/\*|\*\/|@param|@return|@example/,
65
-
219
+
66
220
  // Type definitions and interfaces
67
221
  /interface|type|enum|class.*\{/i,
68
-
222
+
69
223
  // Import/export statements
70
224
  /import|export|require|module\.exports/i,
71
-
225
+
72
226
  // Safe message patterns (no actual codes exposed)
73
227
  /instructions sent|sent to|check your|please enter|has been sent|successfully sent|we've sent|click the link|enter the code|will expire/i,
74
-
228
+
75
229
  // Configuration and constants
76
230
  /const\s+\w+\s*=|enum\s+\w+|type\s+\w+/i,
77
-
231
+
78
232
  // Function definitions
79
233
  /function\s+\w+|async\s+\w+|\w+\s*\(/i,
80
-
234
+
81
235
  // Return statements with safe messages
82
236
  /return\s*\{[^}]*success[^}]*\}/i,
83
237
  ];
84
-
238
+
85
239
  // Common safe variable names that might contain keywords
86
240
  this.safeVariableNames = [
87
241
  /^(is|has|can|should|will|enable|disable|show|hide|display).*code/i,
@@ -95,15 +249,15 @@ class S006Analyzer {
95
249
 
96
250
  async analyze(files, language, options = {}) {
97
251
  const violations = [];
98
-
252
+
99
253
  for (const filePath of files) {
100
254
  // Skip test files, build directories, and node_modules
101
255
  if (this.shouldSkipFile(filePath)) {
102
256
  continue;
103
257
  }
104
-
258
+
105
259
  try {
106
- const content = require('fs').readFileSync(filePath, 'utf8');
260
+ const content = require("fs").readFileSync(filePath, "utf8");
107
261
  const fileViolations = this.analyzeFile(content, filePath, options);
108
262
  violations.push(...fileViolations);
109
263
  } catch (error) {
@@ -112,41 +266,55 @@ class S006Analyzer {
112
266
  }
113
267
  }
114
268
  }
115
-
269
+
116
270
  return violations;
117
271
  }
118
272
 
119
273
  shouldSkipFile(filePath) {
120
274
  const skipPatterns = [
121
- 'test/', 'tests/', '__tests__/', '.test.', '.spec.',
122
- 'node_modules/', 'build/', 'dist/', '.next/', 'coverage/',
123
- 'vendor/', 'mocks/', '.mock.'
275
+ "test/",
276
+ "tests/",
277
+ "__tests__/",
278
+ ".test.",
279
+ ".spec.",
280
+ "node_modules/",
281
+ "build/",
282
+ "dist/",
283
+ ".next/",
284
+ "coverage/",
285
+ "vendor/",
286
+ "mocks/",
287
+ ".mock.",
124
288
  // Removed 'fixtures/' to allow testing
125
289
  ];
126
-
127
- return skipPatterns.some(pattern => filePath.includes(pattern));
290
+
291
+ return skipPatterns.some((pattern) => filePath.includes(pattern));
128
292
  }
129
293
 
130
294
  analyzeFile(content, filePath, options = {}) {
131
295
  const violations = [];
132
- const lines = content.split('\n');
133
-
296
+ const lines = content.split("\n");
297
+
134
298
  lines.forEach((line, index) => {
135
299
  const lineNumber = index + 1;
136
300
  const trimmedLine = line.trim();
137
-
301
+
138
302
  // Skip comments, imports, and empty lines
139
303
  if (this.shouldSkipLine(trimmedLine)) {
140
304
  return;
141
305
  }
142
-
306
+
143
307
  // Check for potential plaintext code transmission
144
- const violation = this.checkForPlaintextCodeTransmission(line, lineNumber, filePath);
308
+ const violation = this.checkForPlaintextCodeTransmission(
309
+ line,
310
+ lineNumber,
311
+ filePath
312
+ );
145
313
  if (violation) {
146
314
  violations.push(violation);
147
315
  }
148
316
  });
149
-
317
+
150
318
  return violations;
151
319
  }
152
320
 
@@ -154,30 +322,34 @@ class S006Analyzer {
154
322
  // Skip comments, imports, and other non-code lines
155
323
  return (
156
324
  line.length === 0 ||
157
- line.startsWith('//') ||
158
- line.startsWith('/*') ||
159
- line.startsWith('*') ||
160
- line.startsWith('import ') ||
161
- line.startsWith('export ') ||
162
- line.startsWith('require(') ||
163
- line.includes('module.exports')
325
+ line.startsWith("//") ||
326
+ line.startsWith("/*") ||
327
+ line.startsWith("*") ||
328
+ line.startsWith("import ") ||
329
+ line.startsWith("export ") ||
330
+ line.startsWith("require(") ||
331
+ line.includes("module.exports")
164
332
  );
165
333
  }
166
334
 
167
335
  checkForPlaintextCodeTransmission(line, lineNumber, filePath) {
168
336
  const lowerLine = line.toLowerCase();
169
-
337
+
170
338
  // First check if line contains safe patterns (early exit)
171
339
  if (this.containsSafePattern(line)) {
172
340
  return null;
173
341
  }
174
-
342
+
175
343
  // Check for variable assignments with sensitive names
176
- const sensitiveAssignment = this.checkSensitiveAssignment(line, lineNumber, filePath);
344
+ const sensitiveAssignment = this.checkSensitiveAssignment(
345
+ line,
346
+ lineNumber,
347
+ filePath
348
+ );
177
349
  if (sensitiveAssignment) {
178
350
  return sensitiveAssignment;
179
351
  }
180
-
352
+
181
353
  // Check for direct plaintext patterns
182
354
  for (const pattern of this.plaintextPatterns) {
183
355
  if (pattern.test(line)) {
@@ -185,67 +357,73 @@ class S006Analyzer {
185
357
  if (this.hasTransmissionContext(line)) {
186
358
  return {
187
359
  ruleId: this.ruleId,
188
- severity: 'error',
189
- message: 'Recovery/activation codes should not be transmitted in plaintext. Use encrypted channels or hash the codes.',
360
+ severity: "error",
361
+ message:
362
+ "Recovery/activation codes should not be transmitted in plaintext. Use encrypted channels or hash the codes.",
190
363
  line: lineNumber,
191
364
  column: this.findPatternColumn(line, pattern),
192
365
  filePath: filePath,
193
- type: 'plaintext_code_transmission',
194
- details: 'Consider using encrypted communication or sending only hashed/masked versions of sensitive codes.'
366
+ type: "plaintext_code_transmission",
367
+ details:
368
+ "Consider using encrypted communication or sending only hashed/masked versions of sensitive codes.",
195
369
  };
196
370
  }
197
371
  }
198
372
  }
199
-
373
+
200
374
  return null;
201
375
  }
202
376
 
203
377
  containsSafePattern(line) {
204
- return this.safePatterns.some(pattern => pattern.test(line));
378
+ return this.safePatterns.some((pattern) => pattern.test(line));
205
379
  }
206
380
 
207
381
  checkSensitiveAssignment(line, lineNumber, filePath) {
208
382
  // Look for variable assignments that combine sensitive codes with transmission
209
- const assignmentMatch = line.match(/(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*(.+)/);
383
+ const assignmentMatch = line.match(
384
+ /(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*(.+)/
385
+ );
210
386
  if (!assignmentMatch) {
211
387
  return null;
212
388
  }
213
-
389
+
214
390
  const [, variableName, valueExpr] = assignmentMatch;
215
391
  const lowerVarName = variableName.toLowerCase();
216
392
  const lowerValueExpr = valueExpr.toLowerCase();
217
-
393
+
218
394
  // Skip safe variable names
219
- if (this.safeVariableNames.some(pattern => pattern.test(lowerVarName))) {
395
+ if (this.safeVariableNames.some((pattern) => pattern.test(lowerVarName))) {
220
396
  return null;
221
397
  }
222
-
398
+
223
399
  // Check if variable name suggests code transmission
224
- const hasSensitiveCodeKeyword = this.sensitiveCodeKeywords.some(keyword =>
400
+ const hasSensitiveCodeKeyword = this.sensitiveCodeKeywords.some((keyword) =>
225
401
  lowerVarName.includes(keyword)
226
402
  );
227
-
228
- const hasSendingKeyword = this.sendingKeywords.some(keyword =>
229
- lowerVarName.includes(keyword) || lowerValueExpr.includes(keyword)
403
+
404
+ const hasSendingKeyword = this.sendingKeywords.some(
405
+ (keyword) =>
406
+ lowerVarName.includes(keyword) || lowerValueExpr.includes(keyword)
230
407
  );
231
-
408
+
232
409
  if (hasSensitiveCodeKeyword && hasSendingKeyword) {
233
410
  // Check if the value looks like it contains actual codes or sensitive data
234
411
  if (this.valueContainsCodes(valueExpr)) {
235
412
  return {
236
413
  ruleId: this.ruleId,
237
- severity: 'warning',
414
+ severity: "warning",
238
415
  message: `Variable '${variableName}' appears to handle sensitive codes for transmission. Ensure codes are encrypted or hashed.`,
239
416
  line: lineNumber,
240
417
  column: line.indexOf(variableName) + 1,
241
418
  filePath: filePath,
242
- type: 'sensitive_code_variable',
419
+ type: "sensitive_code_variable",
243
420
  variableName: variableName,
244
- details: 'Consider encrypting sensitive codes before transmission or use secure communication channels.'
421
+ details:
422
+ "Consider encrypting sensitive codes before transmission or use secure communication channels.",
245
423
  };
246
424
  }
247
425
  }
248
-
426
+
249
427
  return null;
250
428
  }
251
429
 
@@ -255,21 +433,21 @@ class S006Analyzer {
255
433
  /res\.(?:send|json|status|end)/i,
256
434
  /response\.(?:send|json|status|end)/i,
257
435
  /return.*(?:json|response|status)/i,
258
-
436
+
259
437
  // Email/SMS functions
260
438
  /(?:sendEmail|sendSMS|sendMessage|notify|mail)/i,
261
-
439
+
262
440
  // Template rendering
263
441
  /render|template|view|html|email/i,
264
-
442
+
265
443
  // API responses
266
444
  /\.json\(|\.send\(|\.end\(/,
267
-
445
+
268
446
  // String concatenation or template literals with codes
269
447
  /\+.*['"`]|['"`].*\+|\$\{.*\}/,
270
448
  ];
271
-
272
- return transmissionIndicators.some(indicator => indicator.test(line));
449
+
450
+ return transmissionIndicators.some((indicator) => indicator.test(line));
273
451
  }
274
452
 
275
453
  valueContainsCodes(valueExpr) {
@@ -277,24 +455,24 @@ class S006Analyzer {
277
455
  const codePatterns = [
278
456
  // Template strings with variables
279
457
  /\$\{[^}]+\}/,
280
-
458
+
281
459
  // String concatenation
282
460
  /\+\s*[a-zA-Z_$]/,
283
-
461
+
284
462
  // Function calls that might return codes
285
463
  /\w+\([^)]*\)/,
286
-
464
+
287
465
  // Property access that might be codes
288
466
  /\w+\.\w+/,
289
-
467
+
290
468
  // Array/object access
291
469
  /\[.*\]/,
292
-
470
+
293
471
  // Direct string literals that look like codes (6+ chars with mixed case/numbers)
294
472
  /['"`][a-zA-Z0-9]{6,}['"`]/,
295
473
  ];
296
-
297
- return codePatterns.some(pattern => pattern.test(valueExpr));
474
+
475
+ return codePatterns.some((pattern) => pattern.test(valueExpr));
298
476
  }
299
477
 
300
478
  findPatternColumn(line, pattern) {