@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.
- package/config/rule-analysis-strategies.js +18 -2
- package/engines/eslint-engine.js +9 -11
- package/engines/heuristic-engine.js +55 -31
- package/package.json +2 -1
- package/rules/README.md +252 -0
- package/rules/common/C002_no_duplicate_code/analyzer.js +65 -0
- package/rules/common/C002_no_duplicate_code/config.json +23 -0
- package/rules/common/C003_no_vague_abbreviations/analyzer.js +418 -0
- package/rules/common/C003_no_vague_abbreviations/config.json +35 -0
- package/rules/common/C006_function_naming/analyzer.js +504 -0
- package/rules/common/C006_function_naming/config.json +86 -0
- package/rules/common/C006_function_naming/smart-analyzer.js +503 -0
- package/rules/common/C010_limit_block_nesting/analyzer.js +389 -0
- package/rules/common/C012_command_query_separation/analyzer.js +481 -0
- package/rules/common/C012_command_query_separation/ast-analyzer.js +495 -0
- package/rules/common/C013_no_dead_code/analyzer.js +206 -0
- package/rules/common/C014_dependency_injection/analyzer.js +338 -0
- package/rules/common/C017_constructor_logic/analyzer.js +314 -0
- package/rules/common/C019_log_level_usage/analyzer.js +362 -0
- package/rules/common/C019_log_level_usage/config.json +121 -0
- package/rules/common/C029_catch_block_logging/analyzer-backup.js +426 -0
- package/rules/common/C029_catch_block_logging/analyzer-fixed.js +130 -0
- package/rules/common/C029_catch_block_logging/analyzer-multi-tech.js +487 -0
- package/rules/common/C029_catch_block_logging/analyzer-simple.js +110 -0
- package/rules/common/C029_catch_block_logging/analyzer-smart-pipeline.js +755 -0
- package/rules/common/C029_catch_block_logging/analyzer.js +129 -0
- package/rules/common/C029_catch_block_logging/ast-analyzer-backup.js +441 -0
- package/rules/common/C029_catch_block_logging/ast-analyzer-new.js +127 -0
- package/rules/common/C029_catch_block_logging/ast-analyzer.js +133 -0
- package/rules/common/C029_catch_block_logging/cfg-analyzer.js +408 -0
- package/rules/common/C029_catch_block_logging/config.json +59 -0
- package/rules/common/C029_catch_block_logging/dataflow-analyzer.js +454 -0
- package/rules/common/C029_catch_block_logging/multi-language-ast-engine.js +700 -0
- package/rules/common/C029_catch_block_logging/pattern-learning-analyzer.js +568 -0
- package/rules/common/C029_catch_block_logging/semantic-analyzer.js +459 -0
- package/rules/common/C031_validation_separation/analyzer.js +186 -0
- package/rules/common/C041_no_sensitive_hardcode/analyzer.js +292 -0
- package/rules/common/C041_no_sensitive_hardcode/ast-analyzer.js +296 -0
- package/rules/common/C042_boolean_name_prefix/analyzer.js +300 -0
- package/rules/common/C043_no_console_or_print/analyzer.js +431 -0
- package/rules/common/C047_no_duplicate_retry_logic/analyzer.js +590 -0
- package/rules/common/C075_explicit_return_types/analyzer.js +103 -0
- package/rules/common/C076_single_test_behavior/analyzer.js +121 -0
- package/rules/docs/C002_no_duplicate_code.md +57 -0
- package/rules/docs/C031_validation_separation.md +72 -0
- package/rules/index.js +155 -0
- package/rules/migration/converter.js +385 -0
- package/rules/migration/mapping.json +164 -0
- package/rules/parser/constants.js +31 -0
- package/rules/parser/file-config.js +80 -0
- package/rules/parser/rule-parser-simple.js +305 -0
- package/rules/parser/rule-parser.js +527 -0
- package/rules/security/S015_insecure_tls_certificate/analyzer.js +150 -0
- package/rules/security/S015_insecure_tls_certificate/ast-analyzer.js +237 -0
- package/rules/security/S023_no_json_injection/analyzer.js +278 -0
- package/rules/security/S023_no_json_injection/ast-analyzer.js +359 -0
- package/rules/security/S026_json_schema_validation/analyzer.js +251 -0
- package/rules/security/S026_json_schema_validation/config.json +27 -0
- package/rules/security/S027_no_hardcoded_secrets/analyzer.js +436 -0
- package/rules/security/S027_no_hardcoded_secrets/config.json +29 -0
- package/rules/security/S029_csrf_protection/analyzer.js +330 -0
- package/rules/tests/C002_no_duplicate_code.test.js +50 -0
- package/rules/universal/C010/generic.js +0 -0
- package/rules/universal/C010/tree-sitter-analyzer.js +0 -0
- package/rules/utils/ast-utils.js +191 -0
- package/rules/utils/base-analyzer.js +98 -0
- package/rules/utils/pattern-matchers.js +239 -0
- package/rules/utils/rule-helpers.js +264 -0
- package/rules/utils/severity-constants.js +93 -0
- package/scripts/generate_insights.js +188 -0
- package/scripts/merge-reports.js +0 -424
- package/scripts/test-scripts/README.md +0 -22
- package/scripts/test-scripts/test-c041-comparison.js +0 -114
- package/scripts/test-scripts/test-c041-eslint.js +0 -67
- package/scripts/test-scripts/test-eslint-rules.js +0 -146
- package/scripts/test-scripts/test-real-world.js +0 -44
- 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;
|