@sun-asterisk/sunlint 1.2.1 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/config/rule-analysis-strategies.js +18 -2
  2. package/engines/eslint-engine.js +9 -11
  3. package/engines/heuristic-engine.js +55 -31
  4. package/package.json +2 -1
  5. package/rules/README.md +252 -0
  6. package/rules/common/C002_no_duplicate_code/analyzer.js +65 -0
  7. package/rules/common/C002_no_duplicate_code/config.json +23 -0
  8. package/rules/common/C003_no_vague_abbreviations/analyzer.js +418 -0
  9. package/rules/common/C003_no_vague_abbreviations/config.json +35 -0
  10. package/rules/common/C006_function_naming/analyzer.js +504 -0
  11. package/rules/common/C006_function_naming/config.json +86 -0
  12. package/rules/common/C006_function_naming/smart-analyzer.js +503 -0
  13. package/rules/common/C010_limit_block_nesting/analyzer.js +389 -0
  14. package/rules/common/C012_command_query_separation/analyzer.js +481 -0
  15. package/rules/common/C012_command_query_separation/ast-analyzer.js +495 -0
  16. package/rules/common/C013_no_dead_code/analyzer.js +206 -0
  17. package/rules/common/C014_dependency_injection/analyzer.js +338 -0
  18. package/rules/common/C017_constructor_logic/analyzer.js +314 -0
  19. package/rules/common/C019_log_level_usage/analyzer.js +362 -0
  20. package/rules/common/C019_log_level_usage/config.json +121 -0
  21. package/rules/common/C029_catch_block_logging/analyzer-backup.js +426 -0
  22. package/rules/common/C029_catch_block_logging/analyzer-fixed.js +130 -0
  23. package/rules/common/C029_catch_block_logging/analyzer-multi-tech.js +487 -0
  24. package/rules/common/C029_catch_block_logging/analyzer-simple.js +110 -0
  25. package/rules/common/C029_catch_block_logging/analyzer-smart-pipeline.js +755 -0
  26. package/rules/common/C029_catch_block_logging/analyzer.js +129 -0
  27. package/rules/common/C029_catch_block_logging/ast-analyzer-backup.js +441 -0
  28. package/rules/common/C029_catch_block_logging/ast-analyzer-new.js +127 -0
  29. package/rules/common/C029_catch_block_logging/ast-analyzer.js +133 -0
  30. package/rules/common/C029_catch_block_logging/cfg-analyzer.js +408 -0
  31. package/rules/common/C029_catch_block_logging/config.json +59 -0
  32. package/rules/common/C029_catch_block_logging/dataflow-analyzer.js +454 -0
  33. package/rules/common/C029_catch_block_logging/multi-language-ast-engine.js +700 -0
  34. package/rules/common/C029_catch_block_logging/pattern-learning-analyzer.js +568 -0
  35. package/rules/common/C029_catch_block_logging/semantic-analyzer.js +459 -0
  36. package/rules/common/C031_validation_separation/analyzer.js +186 -0
  37. package/rules/common/C041_no_sensitive_hardcode/analyzer.js +292 -0
  38. package/rules/common/C041_no_sensitive_hardcode/ast-analyzer.js +296 -0
  39. package/rules/common/C042_boolean_name_prefix/analyzer.js +300 -0
  40. package/rules/common/C043_no_console_or_print/analyzer.js +431 -0
  41. package/rules/common/C047_no_duplicate_retry_logic/analyzer.js +590 -0
  42. package/rules/common/C075_explicit_return_types/analyzer.js +103 -0
  43. package/rules/common/C076_single_test_behavior/analyzer.js +121 -0
  44. package/rules/docs/C002_no_duplicate_code.md +57 -0
  45. package/rules/docs/C031_validation_separation.md +72 -0
  46. package/rules/index.js +155 -0
  47. package/rules/migration/converter.js +385 -0
  48. package/rules/migration/mapping.json +164 -0
  49. package/rules/parser/constants.js +31 -0
  50. package/rules/parser/file-config.js +80 -0
  51. package/rules/parser/rule-parser-simple.js +305 -0
  52. package/rules/parser/rule-parser.js +527 -0
  53. package/rules/security/S015_insecure_tls_certificate/analyzer.js +150 -0
  54. package/rules/security/S015_insecure_tls_certificate/ast-analyzer.js +237 -0
  55. package/rules/security/S023_no_json_injection/analyzer.js +278 -0
  56. package/rules/security/S023_no_json_injection/ast-analyzer.js +359 -0
  57. package/rules/security/S026_json_schema_validation/analyzer.js +251 -0
  58. package/rules/security/S026_json_schema_validation/config.json +27 -0
  59. package/rules/security/S027_no_hardcoded_secrets/analyzer.js +436 -0
  60. package/rules/security/S027_no_hardcoded_secrets/config.json +29 -0
  61. package/rules/security/S029_csrf_protection/analyzer.js +330 -0
  62. package/rules/tests/C002_no_duplicate_code.test.js +50 -0
  63. package/rules/universal/C010/generic.js +0 -0
  64. package/rules/universal/C010/tree-sitter-analyzer.js +0 -0
  65. package/rules/utils/ast-utils.js +191 -0
  66. package/rules/utils/base-analyzer.js +98 -0
  67. package/rules/utils/pattern-matchers.js +239 -0
  68. package/rules/utils/rule-helpers.js +264 -0
  69. package/rules/utils/severity-constants.js +93 -0
  70. package/scripts/generate_insights.js +188 -0
  71. package/scripts/merge-reports.js +0 -424
  72. package/scripts/test-scripts/README.md +0 -22
  73. package/scripts/test-scripts/test-c041-comparison.js +0 -114
  74. package/scripts/test-scripts/test-c041-eslint.js +0 -67
  75. package/scripts/test-scripts/test-eslint-rules.js +0 -146
  76. package/scripts/test-scripts/test-real-world.js +0 -44
  77. package/scripts/test-scripts/test-rules-on-real-projects.js +0 -86
@@ -0,0 +1,527 @@
1
+ const fs = require("fs")
2
+ const { KEYWORDS } = require("./constants")
3
+
4
+ class RuleParser {
5
+ constructor(isMigrateMode = false) {
6
+ this.isMigrateMode = isMigrateMode
7
+ }
8
+
9
+ // Parse a markdown file and extract rules
10
+ parseMarkdownFile(filePath, config) {
11
+ try {
12
+ const content = fs.readFileSync(filePath, "utf8")
13
+ const rules = []
14
+
15
+ // Split content by rule sections (### 📘 Rule)
16
+ const ruleSections = content.split(/### 📘 Rule\s+/)
17
+
18
+ // If no sections found with the prefix, try to parse the whole file as one rule
19
+ if (ruleSections.length <= 1) {
20
+ const ruleMatch = content.match(/### 📘 Rule\s+([A-Za-z]+\d+)\s*[–-]\s*(.+)/)
21
+ if (ruleMatch) {
22
+ const [fullMatch, id, title] = ruleMatch
23
+ const rule = this.parseRuleContent(content, id, title, config)
24
+ if (rule) {
25
+ rules.push(rule)
26
+ }
27
+ } else {
28
+ console.warn("Could not find any rule in file:", filePath)
29
+ }
30
+ return rules
31
+ }
32
+
33
+ // Skip first section (header)
34
+ for (let i = 1; i < ruleSections.length; i++) {
35
+ const section = ruleSections[i]
36
+ const rule = this.parseRuleSection(section, config)
37
+ if (rule) {
38
+ rules.push(rule)
39
+ }
40
+ }
41
+
42
+ return rules
43
+ } catch (error) {
44
+ console.error(`Error parsing file ${filePath}:`, error.message)
45
+ return []
46
+ }
47
+ }
48
+
49
+ // Parse a single rule section
50
+ parseRuleSection(section, config) {
51
+ try {
52
+ const lines = section.split("\n")
53
+ const firstLine = lines[0]
54
+
55
+ // Extract rule ID and title
56
+ const titleMatch = firstLine.match(/^([A-Z]+\d+)\s*[–-]\s*(.+)$/)
57
+ if (!titleMatch) {
58
+ console.warn("Could not parse rule title:", firstLine)
59
+ return null
60
+ }
61
+
62
+ const [, id, title] = titleMatch
63
+ return this.parseRuleContent(section, id, title, config)
64
+ } catch (error) {
65
+ console.error("Error parsing rule section:", error.message)
66
+ return null
67
+ }
68
+ }
69
+
70
+ // Parse rule content and extract all rule information
71
+ parseRuleContent(content, id, title, config) {
72
+ try {
73
+ // Initialize rule object
74
+ const rule = this.createRuleObject(id, title, config)
75
+ const lines = content.split("\n")
76
+
77
+ const parsingState = this.createParsingState()
78
+
79
+ for (let i = 0; i < lines.length; i++) {
80
+ const line = lines[i]
81
+ this.processLine(line, rule, parsingState, lines, i)
82
+ }
83
+
84
+ // In normal mode, read examples and configs from separate files
85
+ if (!this.isMigrateMode) {
86
+ const externalData = this.readExamplesFromFile(rule.id)
87
+ rule.examples = externalData.examples
88
+ rule.configs = externalData.configs
89
+ }
90
+
91
+ console.log(
92
+ `Parsed rule ${rule.id}: ${rule.examples.good.length} good examples, ${rule.examples.bad.length} bad examples, ${Object.keys(rule.configs).length} configs`,
93
+ )
94
+ return rule
95
+ } catch (error) {
96
+ console.error("Error parsing rule content:", error.message)
97
+ return null
98
+ }
99
+ }
100
+
101
+ // Create initial rule object
102
+ createRuleObject(id, title, config) {
103
+ return {
104
+ id: id.trim(),
105
+ title: title.trim(),
106
+ category: config.category,
107
+ language: config.language,
108
+ framework: config.framework,
109
+ description: "",
110
+ details: [],
111
+ tools: [],
112
+ principles: [],
113
+ severity: "major", // default severity
114
+ version: "1.0",
115
+ status: "activated",
116
+ examples: {
117
+ good: [],
118
+ bad: [],
119
+ },
120
+ configs: {},
121
+ }
122
+ }
123
+
124
+ // Create parsing state object
125
+ createParsingState() {
126
+ return {
127
+ currentSection: null,
128
+ inGoodExample: false,
129
+ inBadExample: false,
130
+ inConfig: false,
131
+ currentConfigType: null,
132
+ inCodeBlock: false,
133
+ currentCodeLanguage: null,
134
+ currentCodeContent: [],
135
+ lastHeaderLine: "",
136
+ }
137
+ }
138
+
139
+ // Process a single line during parsing
140
+ processLine(line, rule, state, lines, lineIndex) {
141
+ // Handle code blocks (only in migrate mode)
142
+ if (line.trim().startsWith("```") && this.isMigrateMode) {
143
+ this.handleCodeBlock(line, rule, state, lines, lineIndex)
144
+ return
145
+ }
146
+
147
+ if (state.inCodeBlock && this.isMigrateMode) {
148
+ state.currentCodeContent.push(line)
149
+ return
150
+ }
151
+
152
+ // Check for section headers and update rule properties
153
+ this.processKeywordLine(line, rule, state)
154
+ }
155
+
156
+ // Handle code block processing
157
+ handleCodeBlock(line, rule, state, lines, lineIndex) {
158
+ if (!state.inCodeBlock) {
159
+ // Start of code block
160
+ state.inCodeBlock = true
161
+ const langMatch = line.trim().match(/^```(\w+)/)
162
+ state.currentCodeLanguage = langMatch ? langMatch[1] : "text"
163
+ state.currentCodeContent = []
164
+
165
+ // Store the last header line to determine context
166
+ if (lineIndex > 0) {
167
+ for (let j = lineIndex - 1; j >= 0; j--) {
168
+ const prevLine = lines[j].trim()
169
+ if (prevLine && !prevLine.startsWith("```")) {
170
+ state.lastHeaderLine = prevLine
171
+ break
172
+ }
173
+ }
174
+ }
175
+ } else {
176
+ // End of code block
177
+ this.processEndOfCodeBlock(rule, state)
178
+ }
179
+ }
180
+
181
+ // Process end of code block
182
+ processEndOfCodeBlock(rule, state) {
183
+ state.inCodeBlock = false
184
+ const codeContent = state.currentCodeContent.join("\n")
185
+
186
+ if (state.inGoodExample) {
187
+ rule.examples.good.push({
188
+ language: state.currentCodeLanguage,
189
+ code: codeContent,
190
+ })
191
+ console.log(`Added good example for rule ${rule.id} (${state.currentCodeLanguage})`)
192
+ } else if (state.inBadExample) {
193
+ rule.examples.bad.push({
194
+ language: state.currentCodeLanguage,
195
+ code: codeContent,
196
+ })
197
+ console.log(`Added bad example for rule ${rule.id} (${state.currentCodeLanguage})`)
198
+ } else if (state.inConfig || state.lastHeaderLine.includes("Config")) {
199
+ // This is a config block - extract config type from header
200
+ const configType = this.extractConfigType(state.lastHeaderLine)
201
+ rule.configs[configType] = codeContent
202
+ console.log(`Added config for rule ${rule.id} (${configType}) from header: ${state.lastHeaderLine}`)
203
+ } else if (["json", "xml", "yaml", "toml", "properties"].includes(state.currentCodeLanguage)) {
204
+ // Default config based on language
205
+ rule.configs[state.currentCodeLanguage] = codeContent
206
+ console.log(`Added config for rule ${rule.id} based on language: ${state.currentCodeLanguage}`)
207
+ } else {
208
+ // Default to good example if no clear context
209
+ rule.examples.good.push({
210
+ language: state.currentCodeLanguage,
211
+ code: codeContent,
212
+ })
213
+ console.log(`Added default example for rule ${rule.id} (${state.currentCodeLanguage})`)
214
+ }
215
+
216
+ state.currentCodeContent = []
217
+ state.lastHeaderLine = ""
218
+ state.currentConfigType = null
219
+ }
220
+
221
+ // Process lines with keywords
222
+ processKeywordLine(line, rule, state) {
223
+ if (this.containsKeyword(line, "OBJECTIVE")) {
224
+ rule.description = this.extractValueAfterKeyword(line, "OBJECTIVE")
225
+ this.resetParsingState(state)
226
+ } else if (this.containsKeyword(line, "DETAILS")) {
227
+ state.currentSection = "details"
228
+ const detailText = this.extractValueAfterKeyword(line, "DETAILS")
229
+ if (detailText) {
230
+ rule.details.push(detailText)
231
+ }
232
+ this.resetParsingFlags(state)
233
+ } else if (this.containsKeyword(line, "APPLIES_TO")) {
234
+ const appliesTo = this.extractValueAfterKeyword(line, "APPLIES_TO")
235
+ if (appliesTo) {
236
+ rule.language = this.parseLanguages(appliesTo, rule.language)
237
+ }
238
+ this.resetParsingState(state)
239
+ } else if (this.containsKeyword(line, "TOOLS")) {
240
+ const tools = this.extractValueAfterKeyword(line, "TOOLS")
241
+ if (tools) {
242
+ rule.tools = this.parseCommaSeparatedValues(tools)
243
+ }
244
+ this.resetParsingState(state)
245
+ } else if (this.containsKeyword(line, "PRINCIPLES")) {
246
+ const principles = this.extractValueAfterKeyword(line, "PRINCIPLES")
247
+ if (principles) {
248
+ rule.principles = this.parseCommaSeparatedValues(principles)
249
+ }
250
+ this.resetParsingState(state)
251
+ } else if (this.containsKeyword(line, "VERSION")) {
252
+ rule.version = this.extractValueAfterKeyword(line, "VERSION")
253
+ this.resetParsingState(state)
254
+ } else if (this.containsKeyword(line, "STATUS")) {
255
+ rule.status = this.extractValueAfterKeyword(line, "STATUS")
256
+ this.resetParsingState(state)
257
+ } else if (this.containsKeyword(line, "SEVERITY")) {
258
+ const severity = this.extractValueAfterKeyword(line, "SEVERITY").toLowerCase()
259
+ if (["critical", "major", "minor"].includes(severity)) {
260
+ rule.severity = severity
261
+ }
262
+ this.resetParsingState(state)
263
+ } else if (this.containsKeyword(line, "GOOD_EXAMPLE") && this.isMigrateMode) {
264
+ state.inGoodExample = true
265
+ state.inBadExample = false
266
+ state.inConfig = false
267
+ state.currentSection = null
268
+ console.log(`Found good example section in rule ${rule.id}`)
269
+ } else if (this.containsKeyword(line, "BAD_EXAMPLE") && this.isMigrateMode) {
270
+ state.inBadExample = true
271
+ state.inGoodExample = false
272
+ state.inConfig = false
273
+ state.currentSection = null
274
+ console.log(`Found bad example section in rule ${rule.id}`)
275
+ } else if ((line.includes("Config") || line.includes("config")) && this.isMigrateMode) {
276
+ state.inConfig = true
277
+ state.inGoodExample = false
278
+ state.inBadExample = false
279
+ state.currentSection = null
280
+ state.currentConfigType = this.extractConfigType(line)
281
+ console.log(`Found config section in rule ${rule.id}: ${line} (type: ${state.currentConfigType})`)
282
+ } else if (state.currentSection === "details" && line.trim() && !line.trim().startsWith("**")) {
283
+ rule.details.push(line.trim())
284
+ }
285
+ }
286
+
287
+ // Reset parsing state
288
+ resetParsingState(state) {
289
+ state.currentSection = null
290
+ this.resetParsingFlags(state)
291
+ }
292
+
293
+ // Reset parsing flags
294
+ resetParsingFlags(state) {
295
+ state.inGoodExample = false
296
+ state.inBadExample = false
297
+ state.inConfig = false
298
+ }
299
+
300
+ // Helper function to check if a line contains any keyword pattern
301
+ containsKeyword(line, keywordCategory) {
302
+ if (!line) return false
303
+
304
+ const trimmedLine = line.trim()
305
+ const patterns = KEYWORDS[keywordCategory]
306
+
307
+ for (const pattern of patterns) {
308
+ if (trimmedLine.includes(pattern)) {
309
+ return true
310
+ }
311
+ }
312
+ return false
313
+ }
314
+
315
+ // Helper function to extract value after the keyword pattern
316
+ extractValueAfterKeyword(line, keywordCategory) {
317
+ if (!line) return ""
318
+
319
+ const trimmedLine = line.trim()
320
+ const patterns = KEYWORDS[keywordCategory]
321
+
322
+ for (const pattern of patterns) {
323
+ const index = trimmedLine.indexOf(pattern)
324
+ if (index !== -1) {
325
+ const afterPattern = trimmedLine.substring(index + pattern.length).trim()
326
+ return afterPattern
327
+ }
328
+ }
329
+ return ""
330
+ }
331
+
332
+ // Helper function to parse languages from "Áp dụng" field
333
+ parseLanguages(appliesTo, defaultLanguage) {
334
+ if (!appliesTo || appliesTo.trim() === "") {
335
+ return defaultLanguage
336
+ }
337
+
338
+ // Clean up the string and split by common separators
339
+ const cleaned = appliesTo
340
+ .replace(/[()]/g, "") // Remove parentheses
341
+ .replace(/\s+/g, " ") // Normalize whitespace
342
+ .trim()
343
+
344
+ // Normalize language names
345
+ let languages = this.normalizeLanguageName(cleaned)
346
+ // Remove duplicate language
347
+ languages = languages.filter((lang, index, self) => self.indexOf(lang) === index)
348
+
349
+ // If we found multiple languages, join them
350
+ if (languages.length > 1) {
351
+ return languages.join(", ")
352
+ } else if (languages.length === 1) {
353
+ return languages[0]
354
+ }
355
+
356
+ return defaultLanguage
357
+ }
358
+
359
+ // Normalize language names
360
+ normalizeLanguageName(languageName) {
361
+ return languageName
362
+ .split(/[,;]/)
363
+ .map((lang) => lang.trim())
364
+ .filter((lang) => lang.length > 0)
365
+ .map((lang) => {
366
+ // Normalize language names
367
+ const normalized = lang.toLowerCase()
368
+ if (normalized.includes("java") && !normalized.includes("javascript")) return "java"
369
+ if (normalized.includes("python")) return "python"
370
+ if (normalized.includes("node") || normalized.includes("javascript")) return "javascript"
371
+ if (normalized.includes("typescript")) return "typescript"
372
+ if (normalized.includes("kotlin")) return "kotlin"
373
+ if (normalized.includes("swift")) return "swift"
374
+ if (normalized.includes("dart")) return "dart"
375
+ if (normalized.includes("go")) return "golang"
376
+ if (normalized.includes("php")) return "php"
377
+ if (normalized.includes("ruby")) return "ruby"
378
+ if (normalized.includes("c#") || normalized.includes("csharp")) return "c#"
379
+ if (normalized.includes("rust")) return "rust"
380
+ if (normalized.includes("scala")) return "scala"
381
+
382
+ return normalized.charAt(0).toUpperCase() + normalized.slice(1).toLowerCase()
383
+ })
384
+ }
385
+
386
+ // Parse comma-separated values
387
+ parseCommaSeparatedValues(value) {
388
+ return value
389
+ .split(",")
390
+ .map((item) => item.trim())
391
+ .filter((item) => item)
392
+ }
393
+
394
+ // Helper function to extract config type from header
395
+ extractConfigType(line) {
396
+ if (!line) return "config"
397
+
398
+ const trimmedLine = line.trim().toLowerCase()
399
+
400
+ if (trimmedLine.includes("eslint")) return "eslint"
401
+ if (trimmedLine.includes("tsconfig") || trimmedLine.includes("tslint")) return "typescript"
402
+ if (trimmedLine.includes("sonarqube") || trimmedLine.includes("sonar")) return "sonarqube"
403
+ if (trimmedLine.includes("detekt")) return "detekt"
404
+ if (trimmedLine.includes("pmd")) return "pmd"
405
+ if (trimmedLine.includes("prettier")) return "prettier"
406
+ if (trimmedLine.includes("ktlint")) return "ktlint"
407
+
408
+ return "config"
409
+ }
410
+
411
+ // Read examples and configs from separate files
412
+ readExamplesFromFile(ruleId) {
413
+ const path = require("path")
414
+
415
+ // Try to determine current locale from environment or default to 'vi'
416
+ const currentLocale = process.env.BUILD_LOCALE || "vi"
417
+
418
+ const examplesDir = path.join(__dirname, "../..", "rules", "examples", currentLocale)
419
+ const exampleFilePath = path.join(examplesDir, `${ruleId}.md`)
420
+
421
+ // Fallback to main examples directory if locale-specific doesn't exist
422
+ const fallbackExamplesDir = path.join(__dirname, "../..", "rules", "examples")
423
+ const fallbackExampleFilePath = path.join(fallbackExamplesDir, `${ruleId}.md`)
424
+
425
+ let targetFilePath = exampleFilePath
426
+ if (!fs.existsSync(exampleFilePath) && fs.existsSync(fallbackExampleFilePath)) {
427
+ targetFilePath = fallbackExampleFilePath
428
+ }
429
+
430
+ if (!fs.existsSync(targetFilePath)) {
431
+ return {
432
+ examples: { good: [], bad: [] },
433
+ configs: {},
434
+ }
435
+ }
436
+
437
+ try {
438
+ const content = fs.readFileSync(targetFilePath, "utf8")
439
+ const lines = content.split("\n")
440
+
441
+ const examples = { good: [], bad: [] }
442
+ const configs = {}
443
+
444
+ let inGoodExample = false
445
+ let inBadExample = false
446
+ let inConfig = false
447
+ let currentConfigType = null
448
+ let inCodeBlock = false
449
+ let currentCodeLanguage = null
450
+ let currentCodeContent = []
451
+
452
+ for (let i = 0; i < lines.length; i++) {
453
+ const line = lines[i]
454
+
455
+ // Handle code blocks
456
+ if (line.trim().startsWith("```")) {
457
+ if (!inCodeBlock) {
458
+ // Start of code block
459
+ inCodeBlock = true
460
+ const langMatch = line.trim().match(/^```(\w+)/)
461
+ currentCodeLanguage = langMatch ? langMatch[1] : "text"
462
+ currentCodeContent = []
463
+ } else {
464
+ // End of code block
465
+ inCodeBlock = false
466
+ const codeContent = currentCodeContent.join("\n")
467
+
468
+ if (inGoodExample) {
469
+ examples.good.push({
470
+ language: currentCodeLanguage,
471
+ code: codeContent,
472
+ })
473
+ } else if (inBadExample) {
474
+ examples.bad.push({
475
+ language: currentCodeLanguage,
476
+ code: codeContent,
477
+ })
478
+ } else if (inConfig && currentConfigType) {
479
+ configs[currentConfigType] = codeContent
480
+ }
481
+
482
+ currentCodeContent = []
483
+ }
484
+ continue
485
+ }
486
+
487
+ if (inCodeBlock) {
488
+ currentCodeContent.push(line)
489
+ continue
490
+ }
491
+
492
+ // Check for section headers - support multiple languages
493
+ if (this.containsKeyword(line, "GOOD_EXAMPLE") || line.includes("Good Examples") || line.includes("良い例")) {
494
+ inGoodExample = true
495
+ inBadExample = false
496
+ inConfig = false
497
+ } else if (
498
+ this.containsKeyword(line, "BAD_EXAMPLE") ||
499
+ line.includes("Bad Examples") ||
500
+ line.includes("悪い例")
501
+ ) {
502
+ inBadExample = true
503
+ inGoodExample = false
504
+ inConfig = false
505
+ } else if (this.containsKeyword(line, "CONFIG") || line.includes("Config")) {
506
+ inConfig = true
507
+ inGoodExample = false
508
+ inBadExample = false
509
+ currentConfigType = this.extractConfigType(line)
510
+ }
511
+ }
512
+
513
+ console.log(
514
+ ` 📖 Read examples for ${ruleId} (${currentLocale}): ${examples.good.length} good, ${examples.bad.length} bad, ${Object.keys(configs).length} configs`,
515
+ )
516
+ return { examples, configs }
517
+ } catch (error) {
518
+ console.error(`Error reading examples for ${ruleId}:`, error.message)
519
+ return {
520
+ examples: { good: [], bad: [] },
521
+ configs: {},
522
+ }
523
+ }
524
+ }
525
+ }
526
+
527
+ module.exports = { RuleParser }
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Hybrid analyzer for S015 - Insecure TLS Certificate Detection
3
+ * Uses AST analysis with regex fallback for comprehensive coverage
4
+ */
5
+
6
+ const S015ASTAnalyzer = require('./ast-analyzer');
7
+
8
+ class S015Analyzer {
9
+ constructor() {
10
+ this.ruleId = 'S015';
11
+ this.ruleName = 'Insecure TLS Certificate';
12
+ this.description = 'Prevent usage of insecure TLS certificate configurations';
13
+ this.astAnalyzer = new S015ASTAnalyzer();
14
+ }
15
+
16
+ async analyze(files, language, options = {}) {
17
+ const violations = [];
18
+
19
+ if (options.verbose) {
20
+ console.log(`🔍 Running S015 analysis on ${files.length} files...`);
21
+ }
22
+
23
+ // Use AST analysis as primary method
24
+ const astViolations = await this.astAnalyzer.analyze(files, language, options);
25
+ violations.push(...astViolations);
26
+
27
+ // Add regex-based patterns for edge cases AST might miss
28
+ for (const filePath of files) {
29
+ try {
30
+ const content = require('fs').readFileSync(filePath, 'utf8');
31
+ const regexViolations = this.analyzeWithRegexPatterns(content, filePath, options);
32
+
33
+ // Filter out duplicates (same line, same type)
34
+ const filteredRegexViolations = regexViolations.filter(regexViolation =>
35
+ !astViolations.some(astViolation =>
36
+ astViolation.line === regexViolation.line &&
37
+ astViolation.filePath === regexViolation.filePath
38
+ )
39
+ );
40
+
41
+ violations.push(...filteredRegexViolations);
42
+ } catch (error) {
43
+ if (options.verbose) {
44
+ console.warn(`⚠️ S015 regex analysis failed for ${filePath}: ${error.message}`);
45
+ }
46
+ }
47
+ }
48
+
49
+ if (options.verbose && violations.length > 0) {
50
+ console.log(`📊 S015 found ${violations.length} violations`);
51
+ }
52
+
53
+ return violations;
54
+ }
55
+
56
+ analyzeWithRegexPatterns(content, filePath, options = {}) {
57
+ const violations = [];
58
+ const lines = content.split('\n');
59
+
60
+ lines.forEach((line, index) => {
61
+ const lineNumber = index + 1;
62
+
63
+ // Pattern 1: Direct rejectUnauthorized: false
64
+ if (/rejectUnauthorized\s*:\s*false/i.test(line)) {
65
+ violations.push({
66
+ ruleId: this.ruleId,
67
+ severity: 'error',
68
+ message: 'Untrusted/self-signed/expired certificate accepted. Only use trusted certificates in production.',
69
+ line: lineNumber,
70
+ column: line.search(/rejectUnauthorized/i) + 1,
71
+ filePath: filePath,
72
+ type: 'reject_unauthorized_false'
73
+ });
74
+ }
75
+
76
+ // Pattern 2: process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
77
+ if (/NODE_TLS_REJECT_UNAUTHORIZED\s*=\s*['"]0['"]/.test(line)) {
78
+ violations.push({
79
+ ruleId: this.ruleId,
80
+ severity: 'error',
81
+ message: 'TLS certificate rejection disabled globally. This affects all HTTPS requests.',
82
+ line: lineNumber,
83
+ column: line.search(/NODE_TLS_REJECT_UNAUTHORIZED/) + 1,
84
+ filePath: filePath,
85
+ type: 'global_tls_disable'
86
+ });
87
+ }
88
+
89
+ // Pattern 3: Insecure TLS versions in configuration
90
+ const tlsVersionPattern = /secureProtocol\s*:\s*['"](?:SSLv2|SSLv3|TLSv1_method|TLSv1)['"]|minVersion\s*:\s*['"](?:TLSv1|TLSv1\.0|TLSv1\.1|SSLv3)['"]|maxVersion\s*:\s*['"](?:TLSv1|TLSv1\.0|TLSv1\.1)['"]|version\s*:\s*['"](?:TLSv1|TLSv1\.0|TLSv1\.1|SSLv3)['"]/i;
91
+ if (tlsVersionPattern.test(line)) {
92
+ violations.push({
93
+ ruleId: this.ruleId,
94
+ severity: 'error',
95
+ message: 'Insecure TLS/SSL version detected. Use TLS 1.2 or TLS 1.3 only.',
96
+ line: lineNumber,
97
+ column: line.search(tlsVersionPattern) + 1,
98
+ filePath: filePath,
99
+ type: 'insecure_tls_version'
100
+ });
101
+ }
102
+
103
+ // Pattern 4: Weak cipher suites
104
+ const weakCipherPattern = /ciphers\s*:\s*['"][^'"]*(?:NULL|RC4|DES|MD5|EXPORT)[^'"]*['"]|cipher\s*:\s*['"][^'"]*(?:NULL|RC4|DES|MD5|EXPORT)[^'"]*['"]/i;
105
+ if (weakCipherPattern.test(line)) {
106
+ violations.push({
107
+ ruleId: this.ruleId,
108
+ severity: 'warning',
109
+ message: 'Weak cipher detected in TLS configuration. Use strong ciphers only.',
110
+ line: lineNumber,
111
+ column: line.search(weakCipherPattern) + 1,
112
+ filePath: filePath,
113
+ type: 'weak_cipher'
114
+ });
115
+ }
116
+
117
+ // Pattern 5: Disabled certificate verification in various contexts
118
+ const certVerificationPattern = /checkServerIdentity\s*:\s*(?:false|null)|verify\s*:\s*false|strictSSL\s*:\s*false|secureOptions\s*:\s*0/i;
119
+ if (certVerificationPattern.test(line)) {
120
+ violations.push({
121
+ ruleId: this.ruleId,
122
+ severity: 'warning',
123
+ message: 'Certificate verification disabled. This may allow man-in-the-middle attacks.',
124
+ line: lineNumber,
125
+ column: line.search(certVerificationPattern) + 1,
126
+ filePath: filePath,
127
+ type: 'cert_verification_disabled'
128
+ });
129
+ }
130
+
131
+ // Pattern 6: Axios/HTTP client insecure configuration
132
+ const httpClientInsecurePattern = /(?:axios|request|fetch|http)\.(?:create|defaults|get|post|put|delete)\s*\([^)]*rejectUnauthorized\s*:\s*false|(?:axios|request)\.defaults\.httpsAgent.*rejectUnauthorized\s*:\s*false/i;
133
+ if (httpClientInsecurePattern.test(line)) {
134
+ violations.push({
135
+ ruleId: this.ruleId,
136
+ severity: 'error',
137
+ message: 'HTTP client configured to ignore certificate errors. This is insecure.',
138
+ line: lineNumber,
139
+ column: line.search(httpClientInsecurePattern) + 1,
140
+ filePath: filePath,
141
+ type: 'http_client_insecure'
142
+ });
143
+ }
144
+ });
145
+
146
+ return violations;
147
+ }
148
+ }
149
+
150
+ module.exports = S015Analyzer;