@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
@@ -1,436 +1,250 @@
1
- /**
2
- * Heuristic analyzer for: S027 – No Hardcoded Secrets
3
- * Purpose: Prevent hardcoded passwords, API keys, secrets while avoiding false positives
4
- * Based on user feedback: avoid flagging state variables, route names, input types
5
- */
1
+ const fs = require('fs');
2
+ const path = require('path');
6
3
 
7
- class S027Analyzer {
4
+ class S027CategorizedAnalyzer {
8
5
  constructor() {
9
6
  this.ruleId = 'S027';
10
- this.ruleName = 'No Hardcoded Secrets';
11
- this.description = 'Không để lộ thông tin bảo mật trong nguồn Git';
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';
12
9
 
13
- // Enhanced keywords that indicate sensitive information
14
- this.sensitiveKeywords = [
15
- 'password', 'pass', 'pwd', 'secret', 'key', 'token',
16
- 'apikey', 'auth', 'credential', 'seed', 'salt',
17
- // Enhanced patterns from roadmap
18
- 'access_token', 'refresh_token', 'id_token', 'bearer',
19
- 'client_secret', 'client_id', 'private_key', 'public_key',
20
- 'encryption_key', 'signing_key', 'session_key',
21
- 'database_password', 'db_password', 'db_pass',
22
- 'aws_secret', 'aws_key', 'github_token', 'slack_token',
23
- 'stripe_key', 'paypal_secret', 'oauth_secret'
24
- ];
25
-
26
- // Patterns that should NOT be flagged (based on user feedback)
27
- this.allowedPatterns = [
28
- // State variables and flags
29
- /^(is|has|enable|show|display|visible|field|strength|valid)/i,
30
- /^_(is|has|enable|show|display)/i,
31
-
32
- // Route/path patterns
33
- /\/(setup|forgot|reset|change|update)-?password/i,
34
- /password\//i,
35
-
36
- // Input type configurations
37
- /type\s*[=:]\s*['"`]password['"`]/i,
38
- /inputtype\s*[=:]\s*['"`]password['"`]/i,
39
-
40
- // Function names and method calls
41
- /^(validate|check|verify|calculate|generate|get|fetch|create)/i,
42
-
43
- // Component/config properties
44
- /^(token|auth|key)type$/i,
45
- /enabled?$/i,
46
- ];
47
-
48
- // Patterns that indicate environment variables or dynamic values
49
- this.dynamicPatterns = [
50
- /process\.env\./i,
51
- /getenv\s*\(/i,
52
- /config\.get\s*\(/i,
53
- /\(\)/i, // Function calls
54
- /await\s+/i,
55
- /\.then\s*\(/i,
56
- ];
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;
57
16
 
58
- // Enhanced secret patterns based on roadmap
59
- this.secretPatterns = [
60
- // API Keys - Enhanced patterns
61
- /(?:api[_-]?key|apikey)['":\s=]+['"]+([a-zA-Z0-9]{20,})['"]+/i,
62
- /['"]+[A-Za-z0-9+\/]{40,}={0,2}['"]+/, // Base64 encoded
63
-
64
- // JWT Tokens
65
- /eyJ[A-Za-z0-9\-_=]+\.[A-Za-z0-9\-_=]+\.?[A-Za-z0-9\-_.+/=]*/,
66
-
67
- // AWS Credentials
68
- /AKIA[0-9A-Z]{16}/,
69
- /['"]+[A-Za-z0-9\/+=]{40}['"]+/, // AWS Secret Key
70
-
71
- // Database URLs with credentials
72
- /(mongodb|mysql|postgres|redis):\/\/[^\/\s'"]+:[^\/\s'"]+@[^\/\s'"]+/,
73
-
74
- // Private Keys
75
- /-----BEGIN [A-Z ]+PRIVATE KEY-----/,
76
-
77
- // GitHub Tokens
78
- /gh[pousr]_[A-Za-z0-9_]{36}/,
79
-
80
- // Slack Tokens
81
- /xox[baprs]-[A-Za-z0-9-]+/,
82
-
83
- // Bearer tokens
84
- /^bearer\s+[a-zA-Z0-9+/=]{10,}$/i,
85
-
86
- // Long alphanumeric strings that look like tokens/keys
87
- /^[a-zA-Z0-9+/=]{20,}$/,
88
-
89
- // API key prefixes
90
- /^(sk|pk|api|key|token)[-_][a-zA-Z0-9]{10,}$/i,
91
-
92
- // Common weak passwords (more flexible)
93
- /^(admin|password|123|root|test|user|pass|secret|key|token)\d*$/i,
94
-
95
- // Mixed alphanumeric secrets (6+ chars with both letters and numbers)
96
- /^[a-zA-Z0-9]*[a-zA-Z][a-zA-Z0-9]*[0-9][a-zA-Z0-9]*$|^[a-zA-Z0-9]*[0-9][a-zA-Z0-9]*[a-zA-Z][a-zA-Z0-9]*$/,
97
-
98
- // Secret-like strings with hyphens/underscores
99
- /^[a-zA-Z0-9]+-[a-zA-Z0-9]+-[a-zA-Z0-9]+$/,
100
- /^[a-zA-Z0-9]+_[a-zA-Z0-9]+_[a-zA-Z0-9]+$/,
101
-
102
- // Generic password patterns
103
- /^.{8,}[a-zA-Z0-9@#$%^&*()!]+$/, // Complex passwords 8+ chars
104
-
105
- // Tokens with specific formats
106
- /^[a-f0-9]{32,}$/i, // Hex tokens
107
- /^[A-Z0-9]{16,}$/, // Uppercase alphanumeric
108
- ];
17
+ // Compile patterns for performance
18
+ this.compilePatterns();
109
19
  }
110
-
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
+
111
45
  async analyze(files, language, options = {}) {
112
46
  const violations = [];
47
+ this.currentFilePath = '';
113
48
 
114
49
  for (const filePath of files) {
115
- // Skip build directories, test files, and node_modules to reduce false positives
116
- if (filePath.includes('build/') || filePath.includes('dist/') ||
117
- filePath.includes('node_modules/') || filePath.includes('.next/') ||
118
- filePath.includes('vendor/') || filePath.includes('coverage/') ||
119
- filePath.includes('.test.') || filePath.includes('.spec.') ||
120
- filePath.includes('test/') || filePath.includes('tests/') ||
121
- filePath.includes('__tests__/') || filePath.includes('.mock.') ||
122
- filePath.includes('mocks/') || filePath.includes('fixtures/')) {
50
+ // Skip build/dist/node_modules
51
+ if (this.shouldSkipFile(filePath)) {
123
52
  continue;
124
53
  }
125
54
 
55
+ this.currentFilePath = filePath;
56
+
126
57
  try {
127
- const content = require('fs').readFileSync(filePath, 'utf8');
58
+ const content = fs.readFileSync(filePath, 'utf8');
128
59
  const fileViolations = this.analyzeFile(content, filePath);
129
60
  violations.push(...fileViolations);
130
61
  } catch (error) {
131
- console.warn(`⚠️ Failed to analyze ${filePath}: ${error.message}`);
62
+ if (options.verbose) {
63
+ console.error(`Error analyzing ${filePath}:`, error.message);
64
+ }
132
65
  }
133
66
  }
134
67
 
135
68
  return violations;
136
69
  }
137
-
138
- analyzeFile(content, filePath) {
139
- const violations = [];
140
- const lines = content.split('\n');
141
-
142
- // Find variable declarations and assignments with sensitive names
143
- const assignments = this.findSensitiveAssignments(lines);
144
-
145
- // Find hardcoded secrets in string literals (new enhancement)
146
- const stringSecrets = this.findSecretsInStrings(lines);
147
-
148
- assignments.forEach(assignment => {
149
- if (this.isHardcodedSecret(assignment)) {
150
- violations.push({
151
- file: filePath,
152
- line: assignment.line,
153
- column: assignment.column,
154
- message: `Avoid hardcoding sensitive information such as '${assignment.variableName}'. Use secure storage instead.`,
155
- severity: 'warning',
156
- ruleId: this.ruleId,
157
- type: 'hardcoded_secret',
158
- variableName: assignment.variableName,
159
- value: assignment.value
160
- });
161
- }
162
- });
163
-
164
- stringSecrets.forEach(secret => {
165
- violations.push({
166
- file: filePath,
167
- line: secret.line,
168
- column: secret.column,
169
- message: `Potential hardcoded secret detected: '${secret.pattern}'. Use secure storage instead.`,
170
- severity: 'warning',
171
- ruleId: this.ruleId,
172
- type: 'hardcoded_string_secret',
173
- pattern: secret.pattern,
174
- value: secret.value
175
- });
176
- });
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
+ ];
177
77
 
178
- return violations;
78
+ return skipPatterns.some(pattern => filePath.includes(pattern));
179
79
  }
180
80
 
181
- findSensitiveAssignments(lines) {
182
- const assignments = [];
183
- const processedLines = new Set(); // Avoid duplicates
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);
184
88
 
185
89
  lines.forEach((line, index) => {
90
+ const lineNumber = index + 1;
186
91
  const trimmedLine = line.trim();
187
- const lineKey = `${index}:${trimmedLine}`;
188
92
 
189
- // Skip comments, imports, and already processed lines
190
- if (this.isCommentOrImport(trimmedLine) || processedLines.has(lineKey)) {
93
+ // Skip comments and imports
94
+ if (this.isCommentOrImport(trimmedLine)) {
191
95
  return;
192
96
  }
193
97
 
194
- // Look for variable declarations: const/let/var name = "value"
195
- const declMatch = trimmedLine.match(/(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*(['"`][^'"`]*['"`]|[^;,\n]+)/);
196
- if (declMatch) {
197
- const [, variableName, valueExpr] = declMatch;
198
- if (this.hasSensitiveKeyword(variableName)) {
199
- assignments.push({
200
- line: index + 1,
201
- column: line.indexOf(variableName) + 1,
202
- variableName,
203
- valueExpr: valueExpr.trim(),
204
- value: this.extractStringValue(valueExpr),
205
- type: 'declaration',
206
- originalLine: line
207
- });
208
- processedLines.add(lineKey);
209
- }
210
- }
211
-
212
- // Look for assignments: name = "value" (but not in declarations)
213
- else {
214
- const assignMatch = trimmedLine.match(/([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*(['"`][^'"`]*['"`]|[^;,\n]+)/);
215
- if (assignMatch && !trimmedLine.match(/(?:const|let|var)\s/)) {
216
- const [, variableName, valueExpr] = assignMatch;
217
- if (this.hasSensitiveKeyword(variableName)) {
218
- assignments.push({
219
- line: index + 1,
220
- column: line.indexOf(variableName) + 1,
221
- variableName,
222
- valueExpr: valueExpr.trim(),
223
- value: this.extractStringValue(valueExpr),
224
- type: 'assignment',
225
- originalLine: line
226
- });
227
- processedLines.add(lineKey);
228
- }
229
- }
230
- }
231
- });
232
-
233
- return assignments;
234
- }
235
-
236
- findSecretsInStrings(lines) {
237
- const secrets = [];
238
-
239
- lines.forEach((line, index) => {
240
- const trimmedLine = line.trim();
241
-
242
- // Skip comments, imports, and test files
243
- if (this.isCommentOrImport(trimmedLine) || this.isTestFile(line)) {
98
+ // Check global exclude patterns first
99
+ if (this.matchesGlobalExcludes(line)) {
244
100
  return;
245
101
  }
246
102
 
247
- // Extract all string literals from the line
248
- const stringLiterals = this.extractStringLiterals(line);
249
-
250
- stringLiterals.forEach(literal => {
251
- // Check if string looks like a secret pattern
252
- const secretPattern = this.detectSecretPattern(literal.value);
253
- if (secretPattern) {
254
- secrets.push({
255
- line: index + 1,
256
- column: literal.column,
257
- pattern: secretPattern,
258
- value: literal.value,
259
- originalLine: line
260
- });
261
- }
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);
262
109
  });
263
110
  });
264
111
 
265
- return secrets;
112
+ return violations;
266
113
  }
267
114
 
268
- extractStringLiterals(line) {
269
- const literals = [];
270
- const stringRegex = /(['"`])([^'"`]*)\1/g;
271
- let match;
272
-
273
- while ((match = stringRegex.exec(line)) !== null) {
274
- const value = match[2];
275
- if (value.length >= 6) { // Only check strings with reasonable length
276
- literals.push({
277
- value: value,
278
- column: match.index + 1
279
- });
280
- }
281
- }
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
+ ];
282
126
 
283
- return literals;
127
+ return testPatterns.some(pattern => pattern.test(filePath));
284
128
  }
285
129
 
286
- detectSecretPattern(value) {
287
- // Enhanced secret detection patterns
288
- const advancedPatterns = [
289
- { name: 'JWT Token', pattern: /^eyJ[A-Za-z0-9\-_=]+\.[A-Za-z0-9\-_=]+\.?[A-Za-z0-9\-_.+/=]*$/ },
290
- { name: 'AWS Access Key', pattern: /^AKIA[0-9A-Z]{16}$/ },
291
- { name: 'GitHub Token', pattern: /^gh[pousr]_[A-Za-z0-9_]{36}$/ },
292
- { name: 'Slack Token', pattern: /^xox[baprs]-[A-Za-z0-9-]+$/ },
293
- { name: 'Base64 Encoded', pattern: /^[A-Za-z0-9+\/]{40,}={0,2}$/ },
294
- { name: 'Private Key', pattern: /-----BEGIN [A-Z ]+PRIVATE KEY-----/ },
295
- { name: 'Database URL', pattern: /(mongodb|mysql|postgres|redis):\/\/[^\/\s'"]+:[^\/\s'"]+@[^\/\s'"]+/ },
296
- { name: 'Long Hex Token', pattern: /^[a-f0-9]{32,}$/i },
297
- { name: 'API Key Format', pattern: /^(sk|pk|api|key|token)[-_][a-zA-Z0-9]{10,}$/i },
298
- // Removed overly aggressive Complex Password pattern
299
- ];
300
-
301
- for (const {name, pattern} of advancedPatterns) {
302
- if (pattern.test(value)) {
303
- return name;
304
- }
305
- }
306
-
307
- return null;
130
+ isCommentOrImport(line) {
131
+ return line.startsWith('//') || line.startsWith('/*') ||
132
+ line.startsWith('import') || line.startsWith('export') ||
133
+ line.startsWith('*') || line.startsWith('<');
308
134
  }
309
135
 
310
- isTestFile(line) {
311
- const testIndicators = ['.spec.', '.test.', '__tests__', 'describe(', 'it(', 'test(', 'expect(', 'jest.', 'mock'];
312
- return testIndicators.some(indicator => line.includes(indicator));
136
+ matchesGlobalExcludes(line) {
137
+ return this.globalExcludePatterns.some(pattern => pattern.test(line));
313
138
  }
314
139
 
315
- findSecretsInStrings(lines) {
316
- const secrets = [];
140
+ checkCategory(category, line, lineNumber, filePath, isTestFile) {
141
+ const violations = [];
317
142
 
318
- lines.forEach((line, index) => {
319
- const trimmedLine = line.trim();
320
-
321
- // Skip comments, imports, and test files
322
- if (this.isCommentOrImport(trimmedLine) || this.isTestFile(trimmedLine)) {
323
- return;
324
- }
143
+ category.compiledPatterns.forEach(({ regex, original }) => {
144
+ let match;
325
145
 
326
- // Extract all string literals from the line
327
- const stringLiterals = this.extractStringLiterals(line);
146
+ // Reset regex lastIndex for global patterns
147
+ regex.lastIndex = 0;
328
148
 
329
- stringLiterals.forEach(literal => {
330
- // Check if string looks like a secret pattern
331
- const secretPattern = this.detectSecretPattern(literal.value);
332
- if (secretPattern) {
333
- secrets.push({
334
- line: index + 1,
335
- column: literal.column,
336
- pattern: secretPattern,
337
- value: literal.value,
338
- originalLine: line
339
- });
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;
340
156
  }
341
- });
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
+ }
342
183
  });
343
184
 
344
- return secrets;
345
- }
346
-
347
- hasSensitiveKeyword(variableName) {
348
- const lowerName = variableName.toLowerCase();
349
- return this.sensitiveKeywords.some(keyword => lowerName.includes(keyword));
350
- }
351
-
352
- isCommentOrImport(line) {
353
- return line.startsWith('//') || line.startsWith('/*') ||
354
- line.startsWith('import') || line.startsWith('export') ||
355
- line.startsWith('*') || line.startsWith('<');
185
+ return violations;
356
186
  }
357
187
 
358
- extractStringValue(valueExpr) {
359
- // Extract string literal value
360
- const stringMatch = valueExpr.match(/^(['"`])([^'"`]*)\1$/);
361
- if (stringMatch) {
362
- return stringMatch[2];
363
- }
364
- return null;
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';
365
197
  }
366
198
 
367
- isHardcodedSecret(assignment) {
368
- const { variableName, value, valueExpr, originalLine } = assignment;
369
-
370
- // 1. Skip if it looks like an allowed pattern (state variables, routes, etc.)
371
- if (this.isAllowedPattern(variableName, originalLine)) {
372
- return false;
373
- }
374
-
375
- // 2. Skip if value comes from environment or dynamic source
376
- if (this.isDynamicValue(valueExpr)) {
377
- return false;
378
- }
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
+ });
379
215
 
380
- // 3. Skip if no string value (e.g., boolean, function call)
381
- if (!value) {
382
- return false;
383
- }
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
+ });
384
221
 
385
- // 4. Check if the value looks like a hardcoded secret
386
- return this.looksLikeSecret(value);
222
+ return stats;
387
223
  }
388
224
 
389
- isAllowedPattern(variableName, originalLine) {
390
- const lowerName = variableName.toLowerCase();
391
- const lowerLine = originalLine.toLowerCase();
392
-
393
- // Check against allowed patterns
394
- if (this.allowedPatterns.some(pattern => pattern.test(lowerName) || pattern.test(lowerLine))) {
395
- return true;
396
- }
397
-
398
- // Remove comments before checking for paths to avoid false matches on "//"
399
- const codeOnlyLine = lowerLine.replace(/\/\/.*$/, '').replace(/\/\*.*?\*\//g, '');
400
-
401
- // Special case: route objects and paths (but not comments with //)
402
- if (codeOnlyLine.includes('route') || codeOnlyLine.includes('path') ||
403
- (codeOnlyLine.includes('/') && !lowerLine.includes('//'))) {
404
- return true;
405
- }
406
-
407
- // Special case: React/component props
408
- if (codeOnlyLine.includes('<') || codeOnlyLine.includes('inputtype') || codeOnlyLine.includes('type=')) {
409
- return true;
225
+ // Method for filtering by category
226
+ filterByCategory(violations, categoryNames) {
227
+ if (!categoryNames || categoryNames.length === 0) {
228
+ return violations;
410
229
  }
411
230
 
412
- return false;
413
- }
414
-
415
- isDynamicValue(valueExpr) {
416
- return this.dynamicPatterns.some(pattern => pattern.test(valueExpr));
231
+ return violations.filter(violation =>
232
+ categoryNames.includes(violation.category)
233
+ );
417
234
  }
418
235
 
419
- looksLikeSecret(value) {
420
- // Skip very short values (likely not secrets)
421
- if (value.length < 6) {
422
- return false;
423
- }
236
+ // Method for filtering by severity
237
+ filterBySeverity(violations, minSeverity = 'info') {
238
+ const severityOrder = ['info', 'warning', 'error'];
239
+ const minIndex = severityOrder.indexOf(minSeverity);
424
240
 
425
- // Skip common non-secret values
426
- const commonValues = ['password', 'bearer', 'basic', 'token', 'key', 'secret', 'admin', 'user'];
427
- if (commonValues.includes(value.toLowerCase())) {
428
- return false;
429
- }
241
+ if (minIndex === -1) return violations;
430
242
 
431
- // Check against secret patterns
432
- return this.secretPatterns.some(pattern => pattern.test(value));
243
+ return violations.filter(violation => {
244
+ const violationIndex = severityOrder.indexOf(violation.severity);
245
+ return violationIndex >= minIndex;
246
+ });
433
247
  }
434
248
  }
435
249
 
436
- module.exports = S027Analyzer;
250
+ module.exports = S027CategorizedAnalyzer;