@sun-asterisk/sunlint 1.3.0 → 1.3.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 (124) hide show
  1. package/CHANGELOG.md +115 -1
  2. package/CONTRIBUTING.md +249 -605
  3. package/README.md +3 -4
  4. package/config/ci-cd.json +54 -0
  5. package/config/development.json +56 -0
  6. package/config/large-project.json +143 -0
  7. package/config/presets/all.json +0 -1
  8. package/config/release.json +70 -0
  9. package/config/rule-analysis-strategies.js +38 -3
  10. package/config/rules/enhanced-rules-registry.json +474 -1179
  11. package/config/rules/rules-registry-generated.json +3 -3
  12. package/core/cli-action-handler.js +24 -30
  13. package/core/cli-program.js +11 -3
  14. package/core/config-merger.js +29 -2
  15. package/core/enhanced-rules-registry.js +3 -2
  16. package/core/semantic-engine.js +129 -19
  17. package/core/semantic-rule-base.js +4 -2
  18. package/core/unified-rule-registry.js +1 -1
  19. package/docs/COMMAND-EXAMPLES.md +134 -0
  20. package/docs/LARGE-PROJECT-GUIDE.md +324 -0
  21. package/engines/heuristic-engine.js +135 -16
  22. package/integrations/eslint/plugin/index.js +0 -2
  23. package/integrations/eslint/plugin/rules/common/c003-no-vague-abbreviations.js +59 -1
  24. package/integrations/eslint/plugin/rules/common/c006-function-name-verb-noun.js +26 -1
  25. package/integrations/eslint/plugin/rules/common/c030-use-custom-error-classes.js +54 -19
  26. package/origin-rules/common-en.md +19 -15
  27. package/package.json +1 -1
  28. package/rules/common/C002_no_duplicate_code/analyzer.js +334 -36
  29. package/rules/common/C003_no_vague_abbreviations/analyzer.js +220 -35
  30. package/rules/common/C006_function_naming/analyzer.js +29 -3
  31. package/rules/common/C010_limit_block_nesting/analyzer.js +181 -337
  32. package/rules/common/C010_limit_block_nesting/config.json +64 -0
  33. package/rules/common/C010_limit_block_nesting/regex-based-analyzer.js +379 -0
  34. package/rules/common/C010_limit_block_nesting/symbol-based-analyzer.js +231 -0
  35. package/rules/common/C013_no_dead_code/analyzer.js +75 -177
  36. package/rules/common/C013_no_dead_code/config.json +61 -0
  37. package/rules/common/C013_no_dead_code/regex-based-analyzer.js +345 -0
  38. package/rules/common/C013_no_dead_code/symbol-based-analyzer.js +640 -0
  39. package/rules/common/C014_dependency_injection/analyzer.js +48 -313
  40. package/rules/common/C014_dependency_injection/config.json +26 -0
  41. package/rules/common/C014_dependency_injection/symbol-based-analyzer.js +751 -0
  42. package/rules/common/C017_constructor_logic/analyzer.js +254 -17
  43. package/rules/common/C017_constructor_logic/semantic-analyzer.js +340 -0
  44. package/rules/common/C018_no_throw_generic_error/analyzer.js +232 -0
  45. package/rules/common/C018_no_throw_generic_error/config.json +50 -0
  46. package/rules/common/C018_no_throw_generic_error/regex-based-analyzer.js +387 -0
  47. package/rules/common/C018_no_throw_generic_error/symbol-based-analyzer.js +314 -0
  48. package/rules/common/C019_log_level_usage/analyzer.js +110 -317
  49. package/rules/common/C019_log_level_usage/pattern-analyzer.js +88 -0
  50. package/rules/common/C019_log_level_usage/system-log-analyzer.js +1267 -0
  51. package/rules/common/C023_no_duplicate_variable/analyzer.js +180 -0
  52. package/rules/common/C023_no_duplicate_variable/config.json +50 -0
  53. package/rules/common/C023_no_duplicate_variable/symbol-based-analyzer.js +158 -0
  54. package/rules/common/C024_no_scatter_hardcoded_constants/analyzer.js +180 -0
  55. package/rules/common/C024_no_scatter_hardcoded_constants/config.json +50 -0
  56. package/rules/common/C024_no_scatter_hardcoded_constants/symbol-based-analyzer.js +181 -0
  57. package/rules/common/C030_use_custom_error_classes/analyzer.js +200 -0
  58. package/rules/common/C033_separate_service_repository/README.md +78 -0
  59. package/rules/common/C033_separate_service_repository/analyzer.js +160 -0
  60. package/rules/common/C033_separate_service_repository/config.json +50 -0
  61. package/rules/common/C033_separate_service_repository/regex-based-analyzer.js +585 -0
  62. package/rules/common/C033_separate_service_repository/symbol-based-analyzer.js +368 -0
  63. package/rules/common/C035_error_logging_context/STRATEGY.md +99 -0
  64. package/rules/common/C035_error_logging_context/analyzer.js +232 -0
  65. package/rules/common/C035_error_logging_context/config.json +54 -0
  66. package/rules/common/C035_error_logging_context/regex-based-analyzer.js +299 -0
  67. package/rules/common/C035_error_logging_context/symbol-based-analyzer.js +454 -0
  68. package/rules/common/C040_centralized_validation/analyzer.js +165 -0
  69. package/rules/common/C040_centralized_validation/config.json +46 -0
  70. package/rules/common/C040_centralized_validation/regex-based-analyzer.js +243 -0
  71. package/rules/common/C040_centralized_validation/symbol-based-analyzer.js +416 -0
  72. package/rules/common/{C076_single_test_behavior → C072_single_test_behavior}/analyzer.js +6 -6
  73. package/rules/common/C076_explicit_function_types/README.md +30 -0
  74. package/rules/common/C076_explicit_function_types/analyzer.js +172 -0
  75. package/rules/common/C076_explicit_function_types/config.json +15 -0
  76. package/rules/common/C076_explicit_function_types/semantic-analyzer.js +341 -0
  77. package/rules/index.js +6 -1
  78. package/rules/parser/rule-parser.js +13 -2
  79. package/rules/security/S005_no_origin_auth/README.md +226 -0
  80. package/rules/security/S005_no_origin_auth/analyzer.js +184 -0
  81. package/rules/security/S005_no_origin_auth/ast-analyzer.js +406 -0
  82. package/rules/security/S005_no_origin_auth/config.json +85 -0
  83. package/rules/security/S006_no_plaintext_recovery_codes/README.md +139 -0
  84. package/rules/security/S006_no_plaintext_recovery_codes/analyzer.js +306 -0
  85. package/rules/security/S006_no_plaintext_recovery_codes/config.json +48 -0
  86. package/rules/security/S007_no_plaintext_otp/README.md +198 -0
  87. package/rules/security/S007_no_plaintext_otp/analyzer.js +406 -0
  88. package/rules/security/S007_no_plaintext_otp/config.json +79 -0
  89. package/rules/security/S007_no_plaintext_otp/semantic-analyzer.js +609 -0
  90. package/rules/security/S007_no_plaintext_otp/semantic-config.json +195 -0
  91. package/rules/security/S007_no_plaintext_otp/semantic-wrapper.js +280 -0
  92. package/rules/security/S009_no_insecure_encryption/README.md +158 -0
  93. package/rules/security/S009_no_insecure_encryption/analyzer.js +319 -0
  94. package/rules/security/S009_no_insecure_encryption/config.json +55 -0
  95. package/rules/security/S010_no_insecure_encryption/README.md +224 -0
  96. package/rules/security/S010_no_insecure_encryption/analyzer.js +493 -0
  97. package/rules/security/S010_no_insecure_encryption/config.json +48 -0
  98. package/rules/security/S016_no_sensitive_querystring/STRATEGY.md +149 -0
  99. package/rules/security/S016_no_sensitive_querystring/analyzer.js +276 -0
  100. package/rules/security/S016_no_sensitive_querystring/config.json +127 -0
  101. package/rules/security/S016_no_sensitive_querystring/regex-based-analyzer.js +258 -0
  102. package/rules/security/S016_no_sensitive_querystring/symbol-based-analyzer.js +495 -0
  103. package/rules/security/S027_no_hardcoded_secrets/analyzer.js +180 -366
  104. package/rules/security/S027_no_hardcoded_secrets/categories.json +153 -0
  105. package/rules/security/S027_no_hardcoded_secrets/categorized-analyzer.js +250 -0
  106. package/rules/security/S048_no_current_password_in_reset/README.md +222 -0
  107. package/rules/security/S048_no_current_password_in_reset/analyzer.js +366 -0
  108. package/rules/security/S048_no_current_password_in_reset/config.json +48 -0
  109. package/rules/security/S055_content_type_validation/README.md +176 -0
  110. package/rules/security/S055_content_type_validation/analyzer.js +312 -0
  111. package/rules/security/S055_content_type_validation/config.json +48 -0
  112. package/rules/utils/rule-helpers.js +140 -1
  113. package/scripts/consolidate-config.js +116 -0
  114. package/scripts/prepare-release.sh +1 -1
  115. package/config/rules/rules-registry.json +0 -765
  116. package/docs/ESLINT-INTEGRATION-STRATEGY.md +0 -392
  117. package/docs/FUTURE_PACKAGES.md +0 -83
  118. package/docs/HEURISTIC_VS_AI.md +0 -113
  119. package/docs/PRODUCTION_DEPLOYMENT_ANALYSIS.md +0 -112
  120. package/docs/PRODUCTION_SIZE_IMPACT.md +0 -183
  121. package/docs/RELEASE_GUIDE.md +0 -230
  122. package/docs/STANDARDIZED-CATEGORY-FILTERING.md +0 -156
  123. package/integrations/eslint/plugin/rules/common/c076-single-behavior-per-test.js +0 -254
  124. package/rules/common/C006_function_naming/smart-analyzer.js +0 -503
@@ -0,0 +1,153 @@
1
+ {
2
+ "S027": {
3
+ "categories": [
4
+ {
5
+ "name": "AWS Credentials",
6
+ "severity": "critical",
7
+ "description": "AWS access keys, secret keys, and session tokens",
8
+ "patterns": [
9
+ "AKIA[0-9A-Z]{16}",
10
+ "(aws[-_]?)?(secret[-_]?access[-_]?key|access[-_]?key[-_]?id|awssecretaccesskey|awsaccesskeyid)\\s*[=:]\\s*[\"']?[A-Za-z0-9\\/+=]{20,40}[\"']?",
11
+ "(aws[-_]?)?session[-_]?token\\s*[=:]\\s*[\"']?[A-Za-z0-9\\/+=]{100,}[\"']?"
12
+ ],
13
+ "exclude_patterns": [
14
+ "(test|mock|fake|example|demo)[-_]?aws",
15
+ "AWS_REGION|AWS_DEFAULT_REGION"
16
+ ]
17
+ },
18
+ {
19
+ "name": "JWT & Authentication Tokens",
20
+ "severity": "critical",
21
+ "description": "JWT tokens and authentication credentials",
22
+ "patterns": [
23
+ "eyJ[A-Za-z0-9\\-_=]+\\.[A-Za-z0-9\\-_=]+\\.?[A-Za-z0-9\\-_.+/=]*",
24
+ "(jwt|bearer|auth|authtoken|jwttoken)[-_]?(token|secret)?\\s*[=:]\\s*[\"']?[a-zA-Z0-9\\-_=]{20,}[\"']?",
25
+ "authorization\\s*[=:]\\s*[\"']?(bearer|basic)\\s+[a-zA-Z0-9\\-_=]{10,}[\"']?"
26
+ ]
27
+ },
28
+ {
29
+ "name": "API Keys & Secrets",
30
+ "severity": "high",
31
+ "description": "Generic API keys and secret tokens",
32
+ "patterns": [
33
+ "(api[-_]?key|apikey|secret[-_]?key|secretkey|access[-_]?token|accesstoken)\\s*[=:]\\s*[\"']?[a-zA-Z0-9\\-_=]{16,}[\"']?",
34
+ "(client[-_]?secret|clientsecret|app[-_]?secret|appsecret)\\s*[=:]\\s*[\"']?[a-zA-Z0-9\\-_=]{20,}[\"']?",
35
+ "(private[-_]?key|privatekey|encryption[-_]?key|encryptionkey)\\s*[=:]\\s*[\"']?[a-zA-Z0-9\\-_=]{20,}[\"']?",
36
+ "(password|secret)\\s*[=:]\\s*[\"'][a-zA-Z0-9\\-_=]{6,}[\"']"
37
+ ],
38
+ "exclude_patterns": [
39
+ "(display|row|sort|primary|foreign)[-_]?key",
40
+ "key(value|path|name|code|id|index)",
41
+ "^key$",
42
+ "(test|mock|demo|example).*password",
43
+ "password.*(test|mock|demo|example|123|dummy)",
44
+ "wrongpassword|correctpassword|testpassword"
45
+ ]
46
+ },
47
+ {
48
+ "name": "Database Credentials",
49
+ "severity": "high",
50
+ "description": "Database connection strings and passwords",
51
+ "patterns": [
52
+ "(mongodb|mysql|postgres|redis):\\/\\/[^\\/\\s'\"]+:[^\\/\\s'\"]+@[^\\/\\s'\"]+",
53
+ "(db|database|dbpassword|databasepassword)[-_]?(password|pass|pwd|secret)?\\s*[=:]\\s*[\"']?[a-zA-Z0-9\\-_=]{6,}[\"']?",
54
+ "connection[-_]?string\\s*[=:]\\s*[\"']?[^\"'\\s]{20,}[\"']?"
55
+ ]
56
+ },
57
+ {
58
+ "name": "Third-party Service Keys",
59
+ "severity": "high",
60
+ "description": "GitHub, Slack, Stripe and other service tokens",
61
+ "patterns": [
62
+ "gh[pousr]_[A-Za-z0-9_]{36}",
63
+ "xox[baprs]-[A-Za-z0-9-]+",
64
+ "sk_live_[A-Za-z0-9]{24,}",
65
+ "(github|slack|stripe|paypal)[-_]?(token|key|secret)[\\s:=]+[\"']?[a-zA-Z0-9\\-_=]{16,}[\"']?"
66
+ ]
67
+ },
68
+ {
69
+ "name": "Suspicious Variable Names",
70
+ "severity": "medium",
71
+ "description": "Variables with sensitive naming patterns",
72
+ "patterns": [
73
+ "(client|app|service)[-_]?(id|key|token|secret)[\"']?\\s*[:=]\\s*[\"'][A-Za-z0-9\\-_=]{12,}[\"']?",
74
+ "(oauth|openid)[-_]?(client[-_]?id|secret)[\\s:=]+[\"']?[a-zA-Z0-9\\-_=]{10,}[\"']?"
75
+ ],
76
+ "exclude_patterns": [
77
+ "(send|verify|update|register|reset).*password",
78
+ "password.*(reset|verify|update|first|time)"
79
+ ]
80
+ },
81
+ {
82
+ "name": "Base64 Encoded Secrets",
83
+ "severity": "medium",
84
+ "description": "Potentially encoded sensitive data",
85
+ "patterns": [
86
+ "(?:^|[\\s=:'\"])([A-Za-z0-9+\\/]{64,}={0,2})(?:[\\s'\";}]|$)"
87
+ ],
88
+ "exclude_patterns": [
89
+ "^[a-zA-Z0-9+\\/]*$",
90
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
91
+ "(test|demo|example|sample|mock)",
92
+ "^\\/[a-zA-Z0-9\\/\\-_]+$",
93
+ "^[a-zA-Z0-9\\/\\-_\\.]+$",
94
+ "[a-zA-Z]+[a-zA-Z\\/\\-_]{20,}",
95
+ "[a-zA-Z]+Slice\\/",
96
+ "[a-zA-Z]+Company\\/",
97
+ "[a-zA-Z]+Management\\/",
98
+ "[a-zA-Z]+Component",
99
+ "[a-zA-Z]+Setting",
100
+ "[a-zA-Z]+Match",
101
+ "Selector$",
102
+ "Controller$",
103
+ "Service$",
104
+ "Api$",
105
+ "slice\\/",
106
+ "Component\\/",
107
+ "management\\/",
108
+ "company\\/",
109
+ "import.*from",
110
+ "require\\("
111
+ ]
112
+ },
113
+ {
114
+ "name": "Environment Variables",
115
+ "severity": "low",
116
+ "description": "Public environment variables that might leak info",
117
+ "patterns": [
118
+ "NEXT_PUBLIC_[A-Z0-9_]+[\\s:=]+[\"'][^\"']+[\"']",
119
+ "react_app_[A-Z0-9_]+[\\s:=]+[\"'][^\"']+[\"']"
120
+ ],
121
+ "exclude_patterns": [
122
+ "NODE_ENV|ENV|ENVIRONMENT|MODE|DEBUG"
123
+ ]
124
+ },
125
+ {
126
+ "name": "File Path Leaks",
127
+ "severity": "low",
128
+ "description": "Sensitive file patterns",
129
+ "patterns": [
130
+ "^\\s*['\"]?\\.env(\\.[a-zA-Z0-9_]+)?['\"]?\\s*$",
131
+ "^\\s*['\"]?(secrets?|credentials?|private[-_]?keys?)\\.(json|ya?ml|ts|js)['\"]?\\s*$",
132
+ "^\\s*['\"]?(id_rsa|id_dsa|\\.pem|\\.p12|\\.pfx)['\"]?\\s*$"
133
+ ],
134
+ "exclude_patterns": [
135
+ "process\\.env\\.",
136
+ "import.*from",
137
+ "require\\(",
138
+ "NODE_ENV|ENVIRONMENT|MODE|DEBUG"
139
+ ]
140
+ }
141
+ ],
142
+ "global_exclude_patterns": [
143
+ "(test|mock|fake|dummy|placeholder)\\.(js|ts|jsx|tsx)$",
144
+ "\\.(test|spec|mock)\\.",
145
+ "__tests__|\\/tests?\\/|\\/spec\\/",
146
+ "process\\.env\\.",
147
+ "import.*from.*['\"]",
148
+ "require\\(['\"]"
149
+ ],
150
+ "min_length": 8,
151
+ "max_length": 1000
152
+ }
153
+ }
@@ -0,0 +1,250 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ class S027CategorizedAnalyzer {
5
+ constructor() {
6
+ this.ruleId = 'S027';
7
+ this.ruleName = 'No Hardcoded Secrets (Categorized)';
8
+ this.description = 'Phát hiện thông tin bảo mật theo categories với độ ưu tiên khác nhau';
9
+
10
+ // Load categories config
11
+ this.config = this.loadConfig();
12
+ this.categories = this.config.categories;
13
+ this.globalExcludePatterns = this.config.global_exclude_patterns.map(p => new RegExp(p, 'i'));
14
+ this.minLength = this.config.min_length || 8;
15
+ this.maxLength = this.config.max_length || 1000;
16
+
17
+ // Compile patterns for performance
18
+ this.compilePatterns();
19
+ }
20
+
21
+ loadConfig() {
22
+ const configPath = path.join(__dirname, 'categories.json');
23
+ try {
24
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
25
+ return config.S027;
26
+ } catch (error) {
27
+ console.error('Failed to load S027 categories config:', error.message);
28
+ return { categories: [], global_exclude_patterns: [] };
29
+ }
30
+ }
31
+
32
+ compilePatterns() {
33
+ this.categories.forEach(category => {
34
+ category.compiledPatterns = category.patterns.map(p => ({
35
+ regex: new RegExp(p, 'gm'),
36
+ original: p
37
+ }));
38
+
39
+ if (category.exclude_patterns) {
40
+ category.compiledExcludePatterns = category.exclude_patterns.map(p => new RegExp(p, 'i'));
41
+ }
42
+ });
43
+ }
44
+
45
+ async analyze(files, language, options = {}) {
46
+ const violations = [];
47
+ this.currentFilePath = '';
48
+
49
+ for (const filePath of files) {
50
+ // Skip build/dist/node_modules
51
+ if (this.shouldSkipFile(filePath)) {
52
+ continue;
53
+ }
54
+
55
+ this.currentFilePath = filePath;
56
+
57
+ try {
58
+ const content = fs.readFileSync(filePath, 'utf8');
59
+ const fileViolations = this.analyzeFile(content, filePath);
60
+ violations.push(...fileViolations);
61
+ } catch (error) {
62
+ if (options.verbose) {
63
+ console.error(`Error analyzing ${filePath}:`, error.message);
64
+ }
65
+ }
66
+ }
67
+
68
+ return violations;
69
+ }
70
+
71
+ shouldSkipFile(filePath) {
72
+ const skipPatterns = [
73
+ 'build/', 'dist/', 'node_modules/', '.git/',
74
+ 'coverage/', '.next/', '.cache/', 'tmp/',
75
+ '.lock', '.log', '.min.js', '.bundle.js'
76
+ ];
77
+
78
+ return skipPatterns.some(pattern => filePath.includes(pattern));
79
+ }
80
+
81
+ analyzeFile(content, filePath) {
82
+ const violations = [];
83
+ // Handle different line endings (Windows \r\n, Unix \n, Mac \r)
84
+ const lines = content.split(/\r?\n/);
85
+
86
+ // Check if this is a test file for context
87
+ const isTestFile = this.isTestFile(filePath);
88
+
89
+ lines.forEach((line, index) => {
90
+ const lineNumber = index + 1;
91
+ const trimmedLine = line.trim();
92
+
93
+ // Skip comments and imports
94
+ if (this.isCommentOrImport(trimmedLine)) {
95
+ return;
96
+ }
97
+
98
+ // Check global exclude patterns first
99
+ if (this.matchesGlobalExcludes(line)) {
100
+ return;
101
+ }
102
+
103
+ // Check each category
104
+ this.categories.forEach(category => {
105
+ const categoryViolations = this.checkCategory(
106
+ category, line, lineNumber, filePath, isTestFile
107
+ );
108
+ violations.push(...categoryViolations);
109
+ });
110
+ });
111
+
112
+ return violations;
113
+ }
114
+
115
+ isTestFile(filePath) {
116
+ const testPatterns = [
117
+ /\.(test|spec)\./i,
118
+ /__tests__/i,
119
+ /\/tests?\//i,
120
+ /\/spec\//i,
121
+ /setupTests/i,
122
+ /testSetup/i,
123
+ /test[-_]/i, // Matches test- or test_
124
+ /^.*\/test[^\/]*\.js$/i // Matches files starting with test
125
+ ];
126
+
127
+ return testPatterns.some(pattern => pattern.test(filePath));
128
+ }
129
+
130
+ isCommentOrImport(line) {
131
+ return line.startsWith('//') || line.startsWith('/*') ||
132
+ line.startsWith('import') || line.startsWith('export') ||
133
+ line.startsWith('*') || line.startsWith('<');
134
+ }
135
+
136
+ matchesGlobalExcludes(line) {
137
+ return this.globalExcludePatterns.some(pattern => pattern.test(line));
138
+ }
139
+
140
+ checkCategory(category, line, lineNumber, filePath, isTestFile) {
141
+ const violations = [];
142
+
143
+ category.compiledPatterns.forEach(({ regex, original }) => {
144
+ let match;
145
+
146
+ // Reset regex lastIndex for global patterns
147
+ regex.lastIndex = 0;
148
+
149
+ while ((match = regex.exec(line)) !== null) {
150
+ const matchedText = match[0];
151
+ const column = match.index + 1;
152
+
153
+ // Check length constraints
154
+ if (matchedText.length < this.minLength || matchedText.length > this.maxLength) {
155
+ continue;
156
+ }
157
+
158
+ // Check category-specific excludes
159
+ if (category.compiledExcludePatterns &&
160
+ category.compiledExcludePatterns.some(pattern => pattern.test(matchedText))) {
161
+ continue;
162
+ }
163
+
164
+ // Be more lenient in test files for lower severity categories
165
+ // But still report critical/high severity issues even in test files
166
+ if (isTestFile && category.severity === 'low') {
167
+ continue;
168
+ }
169
+
170
+ violations.push({
171
+ file: filePath,
172
+ line: lineNumber,
173
+ column: column,
174
+ message: `[${category.name}] Potential ${category.severity} security risk: '${matchedText}'. ${category.description}`,
175
+ severity: this.mapSeverity(category.severity),
176
+ ruleId: this.ruleId,
177
+ category: category.name,
178
+ categoryDescription: category.description,
179
+ matchedPattern: original,
180
+ matchedText: matchedText
181
+ });
182
+ }
183
+ });
184
+
185
+ return violations;
186
+ }
187
+
188
+ mapSeverity(categorySeverity) {
189
+ const severityMap = {
190
+ 'critical': 'error',
191
+ 'high': 'warning',
192
+ 'medium': 'warning',
193
+ 'low': 'info'
194
+ };
195
+
196
+ return severityMap[categorySeverity] || 'warning';
197
+ }
198
+
199
+ // Method for getting category statistics
200
+ getCategoryStats(violations) {
201
+ const stats = {};
202
+
203
+ violations.forEach(violation => {
204
+ const category = violation.category;
205
+ if (!stats[category]) {
206
+ stats[category] = {
207
+ count: 0,
208
+ severity: violation.severity,
209
+ files: new Set()
210
+ };
211
+ }
212
+ stats[category].count++;
213
+ stats[category].files.add(violation.file);
214
+ });
215
+
216
+ // Convert Set to array for JSON serialization
217
+ Object.keys(stats).forEach(category => {
218
+ stats[category].files = Array.from(stats[category].files);
219
+ stats[category].fileCount = stats[category].files.length;
220
+ });
221
+
222
+ return stats;
223
+ }
224
+
225
+ // Method for filtering by category
226
+ filterByCategory(violations, categoryNames) {
227
+ if (!categoryNames || categoryNames.length === 0) {
228
+ return violations;
229
+ }
230
+
231
+ return violations.filter(violation =>
232
+ categoryNames.includes(violation.category)
233
+ );
234
+ }
235
+
236
+ // Method for filtering by severity
237
+ filterBySeverity(violations, minSeverity = 'info') {
238
+ const severityOrder = ['info', 'warning', 'error'];
239
+ const minIndex = severityOrder.indexOf(minSeverity);
240
+
241
+ if (minIndex === -1) return violations;
242
+
243
+ return violations.filter(violation => {
244
+ const violationIndex = severityOrder.indexOf(violation.severity);
245
+ return violationIndex >= minIndex;
246
+ });
247
+ }
248
+ }
249
+
250
+ module.exports = S027CategorizedAnalyzer;
@@ -0,0 +1,222 @@
1
+ # S048 - No Current Password in Reset Process
2
+
3
+ ## Mô tả
4
+
5
+ Rule này kiểm tra xem các quy trình đặt lại mật khẩu có yêu cầu mật khẩu hiện tại hay không. Việc yêu cầu mật khẩu hiện tại trong quy trình reset mật khẩu vi phạm nguyên tắc bảo mật và làm mất đi mục đích của tính năng "quên mật khẩu".
6
+
7
+ ## Mục tiêu
8
+
9
+ - Ngăn chặn việc yêu cầu mật khẩu hiện tại trong quy trình reset mật khẩu
10
+ - Đảm bảo quy trình reset mật khẩu được thiết kế an toàn và hợp lý
11
+ - Tuân thủ OWASP A04:2021 - Insecure Design và CWE-640
12
+
13
+ ## Chi tiết Rule
14
+
15
+ ### Phát hiện lỗi khi:
16
+
17
+ 1. **API endpoints yêu cầu current password trong reset**:
18
+ ```javascript
19
+ app.post('/reset-password', (req, res) => {
20
+ const { currentPassword, newPassword } = req.body; // ❌ Yêu cầu mật khẩu hiện tại
21
+ if (!validateCurrentPassword(currentPassword)) {
22
+ return res.status(400).json({ error: 'Current password incorrect' });
23
+ }
24
+ });
25
+ ```
26
+
27
+ 2. **Form validation yêu cầu current password**:
28
+ ```typescript
29
+ const resetPasswordSchema = {
30
+ currentPassword: { type: String, required: true }, // ❌ Bắt buộc mật khẩu hiện tại
31
+ newPassword: { type: String, required: true }
32
+ };
33
+ ```
34
+
35
+ 3. **Service methods kiểm tra current password trong reset**:
36
+ ```javascript
37
+ async resetPassword(userId, currentPassword, newPassword) {
38
+ const user = await User.findById(userId);
39
+ if (!user.validatePassword(currentPassword)) { // ❌ Validate mật khẩu hiện tại
40
+ throw new Error('Current password is incorrect');
41
+ }
42
+ }
43
+ ```
44
+
45
+ 4. **React components với current password field**:
46
+ ```typescript
47
+ function ResetPasswordForm() {
48
+ return (
49
+ <form>
50
+ <input name="currentPassword" required /> {/* ❌ Trường mật khẩu hiện tại */}
51
+ <input name="newPassword" required />
52
+ </form>
53
+ );
54
+ }
55
+ ```
56
+
57
+ ### Cách khắc phục:
58
+
59
+ 1. **Sử dụng token-based reset**:
60
+ ```javascript
61
+ app.post('/reset-password', (req, res) => {
62
+ const { token, newPassword } = req.body; // ✅ Sử dụng token thay vì current password
63
+ if (!validateResetToken(token)) {
64
+ return res.status(400).json({ error: 'Invalid reset token' });
65
+ }
66
+ });
67
+ ```
68
+
69
+ 2. **Schema với reset token**:
70
+ ```typescript
71
+ const resetPasswordSchema = {
72
+ resetToken: { type: String, required: true }, // ✅ Token reset an toàn
73
+ newPassword: { type: String, required: true }
74
+ };
75
+ ```
76
+
77
+ 3. **Service method an toàn**:
78
+ ```javascript
79
+ async resetPasswordWithToken(resetToken, newPassword) {
80
+ const tokenData = await validateResetToken(resetToken); // ✅ Validate token
81
+ if (!tokenData.valid) {
82
+ throw new Error('Invalid or expired reset token');
83
+ }
84
+ }
85
+ ```
86
+
87
+ 4. **Form với email verification**:
88
+ ```typescript
89
+ function ForgotPasswordForm() {
90
+ return (
91
+ <form>
92
+ <input name="email" type="email" required /> {/* ✅ Chỉ cần email */}
93
+ <button>Send Reset Link</button>
94
+ </form>
95
+ );
96
+ }
97
+ ```
98
+
99
+ ## Tại sao đây là vấn đề bảo mật?
100
+
101
+ ### 1. **Mâu thuẫn logic**
102
+ - Nếu người dùng nhớ mật khẩu hiện tại, họ không cần reset
103
+ - Yêu cầu mật khẩu hiện tại làm vô hiệu hóa tính năng "quên mật khẩu"
104
+
105
+ ### 2. **Tạo điểm yếu bảo mật**
106
+ - Kẻ tấn công có thể lợi dụng để brute force mật khẩu
107
+ - Tăng surface attack cho account takeover
108
+
109
+ ### 3. **Trải nghiệm người dùng kém**
110
+ - Người dùng quên mật khẩu không thể hoàn thành quy trình reset
111
+ - Dẫn đến khóa tài khoản và frustration
112
+
113
+ ## Các trường hợp ngoại lệ
114
+
115
+ ### Trường hợp hợp lệ (không phải lỗi):
116
+
117
+ 1. **Password Change (không phải Reset)**:
118
+ ```javascript
119
+ // ✅ Thay đổi mật khẩu khi đã đăng nhập - hợp lệ
120
+ app.post('/change-password', authenticateUser, (req, res) => {
121
+ const { currentPassword, newPassword } = req.body;
122
+ // Hợp lệ vì đây là thay đổi, không phải reset
123
+ });
124
+ ```
125
+
126
+ 2. **Profile settings**:
127
+ ```javascript
128
+ // ✅ Cập nhật mật khẩu trong settings - hợp lệ
129
+ function ProfileSettings() {
130
+ return (
131
+ <div>
132
+ <h2>Change Password</h2> {/* Đây là change, không phải reset */}
133
+ <input name="currentPassword" />
134
+ <input name="newPassword" />
135
+ </div>
136
+ );
137
+ }
138
+ ```
139
+
140
+ ## Phương pháp detect
141
+
142
+ Rule này sử dụng **heuristic analysis** với các pattern:
143
+
144
+ 1. **Context Detection**: Phát hiện ngữ cảnh password reset
145
+ - Keywords: `reset`, `forgot`, `recover`, `forgotpassword`
146
+ - Endpoints: `/reset-password`, `/forgot-password`
147
+ - Functions: `resetPassword()`, `forgotPassword()`
148
+
149
+ 2. **Violation Detection**: Tìm yêu cầu current password
150
+ - Field names: `currentPassword`, `oldPassword`, `existingPassword`
151
+ - Validation patterns: `validateCurrentPassword()`, `checkOldPassword()`
152
+ - Schema fields: `currentPassword: { required: true }`
153
+
154
+ 3. **Context Filtering**: Loại bỏ false positives
155
+ - Bỏ qua password change contexts
156
+ - Bỏ qua test files và documentation
157
+ - Bỏ qua comments và type definitions
158
+
159
+ ## Tham khảo
160
+
161
+ - [OWASP A04:2021 - Insecure Design](https://owasp.org/Top10/A04_2021-Insecure_Design/)
162
+ - [CWE-640: Weak Password Recovery Mechanism for Forgotten Password](https://cwe.mitre.org/data/definitions/640.html)
163
+ - [OWASP Authentication Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html#forgot-password)
164
+ - [NIST SP 800-63B - Digital Identity Guidelines](https://pages.nist.gov/800-63-3/sp800-63b.html#sec5)
165
+
166
+ ## Ví dụ
167
+
168
+ ### Violation Examples
169
+
170
+ ```javascript
171
+ // ❌ Express.js với current password requirement
172
+ app.post('/reset-password', (req, res) => {
173
+ if (!req.body.currentPassword) {
174
+ return res.status(400).json({ error: 'Current password required' });
175
+ }
176
+ });
177
+
178
+ // ❌ NestJS với validation current password
179
+ @Post('reset-password')
180
+ async resetPassword(@Body() data: { currentPassword: string, newPassword: string }) {
181
+ await this.authService.validateCurrentPassword(data.currentPassword);
182
+ }
183
+
184
+ // ❌ Mongoose schema yêu cầu current password
185
+ const resetSchema = new Schema({
186
+ currentPassword: { type: String, required: true }, // Vi phạm
187
+ newPassword: { type: String, required: true }
188
+ });
189
+
190
+ // ❌ React form với current password field
191
+ <input
192
+ name="currentPassword"
193
+ placeholder="Enter current password"
194
+ required
195
+ />
196
+ ```
197
+
198
+ ### Secure Examples
199
+
200
+ ```javascript
201
+ // ✅ Token-based reset
202
+ app.post('/reset-password', (req, res) => {
203
+ const { token, newPassword } = req.body;
204
+ if (!validateResetToken(token)) {
205
+ return res.status(400).json({ error: 'Invalid reset token' });
206
+ }
207
+ });
208
+
209
+ // ✅ Email-based forgot password
210
+ app.post('/forgot-password', (req, res) => {
211
+ const { email } = req.body;
212
+ sendResetEmail(email);
213
+ res.json({ message: 'Reset link sent to email' });
214
+ });
215
+
216
+ // ✅ Secure reset schema
217
+ const resetSchema = new Schema({
218
+ resetToken: { type: String, required: true },
219
+ newPassword: { type: String, required: true },
220
+ tokenExpiry: { type: Date, required: true }
221
+ });
222
+ ```