@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,406 @@
1
+ /**
2
+ * Heuristic analyzer for S007 - No Plaintext OTP
3
+ * Purpose: Detect storing or transmitting OTP codes in plaintext
4
+ * Based on OWASP A02:2021 - Cryptographic Failures
5
+ */
6
+
7
+ class S007Analyzer {
8
+ constructor() {
9
+ this.ruleId = 'S007';
10
+ this.ruleName = 'No Plaintext OTP';
11
+ this.description = 'One-Time Passwords must not be stored in plaintext';
12
+
13
+ // Keywords that indicate OTP/one-time codes
14
+ this.otpKeywords = [
15
+ 'otp', 'totp', 'hotp', 'one.?time', 'onetime', '2fa', 'mfa',
16
+ 'authenticator', 'verification.?code', 'auth.?code', 'sms.?code',
17
+ 'temp.?code', 'temp.?password', 'pin.?code', 'security.?code',
18
+ 'access.?code', 'login.?code', 'token'
19
+ ];
20
+
21
+ // Keywords that indicate storage operations
22
+ this.storageKeywords = [
23
+ 'save', 'store', 'insert', 'update', 'create', 'persist', 'write',
24
+ 'set', 'put', 'add', 'database', 'db', 'collection', 'table',
25
+ 'cache', 'session', 'localStorage', 'sessionStorage', 'cookie',
26
+ 'redis', 'mongo', 'sql', 'query', 'orm'
27
+ ];
28
+
29
+ // Keywords that indicate transmission operations
30
+ this.transmissionKeywords = [
31
+ 'send', 'email', 'sms', 'text', 'message', 'mail', 'push', 'notify',
32
+ 'transmit', 'deliver', 'dispatch', 'forward', 'post', 'response',
33
+ 'json', 'body', 'payload', 'data', 'return', 'res\.', 'api'
34
+ ];
35
+
36
+ // Patterns that indicate plaintext OTP usage
37
+ this.plaintextOtpPatterns = [
38
+ // Storage patterns - storing OTP in plaintext
39
+ /(?:save|store|insert|update|create|persist|write|set|put|add).*(?:otp|totp|hotp|one.?time|onetime|2fa|mfa|auth.?code|verification.?code|sms.?code|temp.?code|security.?code|access.?code|login.?code)/i,
40
+ /(?:otp|totp|hotp|one.?time|onetime|2fa|mfa|auth.?code|verification.?code|sms.?code|temp.?code|security.?code|access.?code|login.?code).*(?:save|store|insert|update|create|persist|write|set|put|add)/i,
41
+
42
+ // Database storage patterns
43
+ /(?:database|db|collection|table|redis|mongo|sql|query|orm).*(?:otp|totp|hotp|one.?time|onetime|2fa|mfa|auth.?code|verification.?code|sms.?code|temp.?code|security.?code|access.?code|login.?code)/i,
44
+ /(?:otp|totp|hotp|one.?time|onetime|2fa|mfa|auth.?code|verification.?code|sms.?code|temp.?code|security.?code|access.?code|login.?code).*(?:database|db|collection|table|redis|mongo|sql|query|orm)/i,
45
+
46
+ // Transmission patterns - sending OTP in plaintext
47
+ /(?:send|email|sms|text|message|mail|push|notify|transmit|deliver|dispatch|forward|post).*(?:otp|totp|hotp|one.?time|onetime|2fa|mfa|auth.?code|verification.?code|sms.?code|temp.?code|security.?code|access.?code|login.?code)/i,
48
+ /(?:otp|totp|hotp|one.?time|onetime|2fa|mfa|auth.?code|verification.?code|sms.?code|temp.?code|security.?code|access.?code|login.?code).*(?:send|email|sms|text|message|mail|push|notify|transmit|deliver|dispatch|forward|post)/i,
49
+
50
+ // Response patterns - returning OTP in API responses
51
+ /(?:response|json|body|payload|data|return|res\.).*(?:otp|totp|hotp|one.?time|onetime|2fa|mfa|auth.?code|verification.?code|sms.?code|temp.?code|security.?code|access.?code|login.?code)/i,
52
+ /(?:otp|totp|hotp|one.?time|onetime|2fa|mfa|auth.?code|verification.?code|sms.?code|temp.?code|security.?code|access.?code|login.?code).*(?:response|json|body|payload|data|return|res\.)/i,
53
+
54
+ // Variable assignments with OTP that are stored/transmitted (not just declared)
55
+ /(?:const|let|var)\s+\w*(?:otp|totp|hotp|one.?time|onetime|2fa|mfa|auth.?code|verification.?code|sms.?code|temp.?code|security.?code|access.?code|login.?code)\w*\s*=\s*['"`][^'"`]+['"`].*(?:save|store|send|transmit|cache|redis|db|database)/i,
56
+
57
+ // String literals containing OTP
58
+ /['"`].*(?:otp|totp|hotp|one.?time|onetime|2fa|mfa|auth.?code|verification.?code|sms.?code|temp.?code|security.?code|access.?code|login.?code).*['"`]/i,
59
+
60
+ // Template strings with OTP
61
+ /\$\{.*(?:otp|totp|hotp|one.?time|onetime|2fa|mfa|auth.?code|verification.?code|sms.?code|temp.?code|security.?code|access.?code|login.?code).*\}/i,
62
+
63
+ // Console/logging with OTP
64
+ /(?:console|log|debug|trace|print).*(?:otp|totp|hotp|one.?time|onetime|2fa|mfa|auth.?code|verification.?code|sms.?code|temp.?code|security.?code|access.?code|login.?code)/i,
65
+
66
+ // Session/localStorage with OTP
67
+ /(?:session|localStorage|sessionStorage|cookie).*(?:otp|totp|hotp|one.?time|onetime|2fa|mfa|auth.?code|verification.?code|sms.?code|temp.?code|security.?code|access.?code|login.?code)/i,
68
+ ];
69
+
70
+ // Patterns that should be excluded (safe practices)
71
+ this.safePatterns = [
72
+ // Hashed or encrypted OTP
73
+ /hash|encrypt|cipher|bcrypt|crypto|secure|salt|hmac|sha|md5|pbkdf2/i,
74
+
75
+ // Environment variables or config (configuration, not storage of actual values)
76
+ /process\.env|config\.|getenv|\.env/i,
77
+
78
+ // Comments and documentation
79
+ /\/\/|\/\*|\*\/|@param|@return|@example|@description|TODO|FIXME/,
80
+
81
+ // Type definitions and interfaces
82
+ /interface|type|enum|class\s+\w+\s*\{|abstract\s+class/i,
83
+
84
+ // Import/export statements
85
+ /import|export|require|module\.exports/i,
86
+
87
+ // Safe message patterns (no actual OTP values exposed)
88
+ /instructions sent|sent to|check your|please enter|has been sent|successfully sent|we've sent|click the link|enter the code|will expire|verify|authenticate/i,
89
+
90
+ // Function/method definitions
91
+ /function\s+\w+|async\s+function|\w+\s*\([^)]*\)\s*\{|=>\s*\{/i,
92
+
93
+ // Safe return statements (status messages, not actual codes)
94
+ /return\s*\{[^}]*(?:success|message|status|sent|verified)[^}]*\}/i,
95
+
96
+ // Time-based checks or validation (not storage)
97
+ /(?:validate|verify|check|compare|match|expired|timeout|ttl|duration)/i,
98
+
99
+ // Method names that don't store plaintext
100
+ /(?:generateOtp|createOtp|validateOtp|verifyOtp|checkOtp|expireOtp|deleteOtp|removeOtp)/i,
101
+
102
+ // Safe operations on OTP (hashing, encrypting, etc.)
103
+ /(?:hash|encrypt|decrypt|cipher|secure|bcrypt|createHash|createHmac).*(?:otp|code|auth)/i,
104
+ /(?:otp|code|auth).*(?:hash|encrypt|decrypt|cipher|secure|bcrypt|Hash|Hmac)/i,
105
+
106
+ // Secure service calls
107
+ /(?:secure|encrypted|safe).*(?:send|email|sms|service)/i,
108
+ /(?:send|email|sms).*(?:secure|encrypted|safe)/i,
109
+
110
+ // Token/session creation from OTP (not storing OTP itself)
111
+ /(?:create|generate).*(?:token|session).*(?:from|with)/i,
112
+ /(?:token|session).*(?:create|generate)/i,
113
+
114
+ // Safe logging patterns
115
+ /log.*(?:success|sent|generated|timestamp|result)/i,
116
+ /console\.(?:log|error|warn|debug).*(?:user|failed|success|validation|error)(?!.*otp.*:)/i,
117
+ /log.*(?:failed|error|validation).*user/i,
118
+
119
+ // Return only metadata, not OTP
120
+ /return.*(?:timestamp|Date|success|message|sent)/i,
121
+
122
+ // Safe crypto operations on OTP (hashing, not exposing)
123
+ /\.update\(.*otp.*\+.*\)|\.update\(otp.*\)|createHmac.*\.update/i,
124
+ /crypto\.create(?:Hash|Hmac).*\.update\(/i,
125
+ ];
126
+
127
+ // Common safe variable names that might contain OTP keywords
128
+ this.safeVariableNames = [
129
+ /^(is|has|can|should|will|enable|disable|show|hide|display|need|require).*(?:otp|code|auth)/i,
130
+ /^.*(?:type|config|setting|option|flag|enabled|disabled|required|valid|invalid|expired)$/i,
131
+ /^(?:otp|code|auth).*(?:type|config|setting|option|flag|enabled|disabled|required|valid|invalid|expired)$/i,
132
+ /^(?:validate|verify|check|generate|create|expire|delete|remove).*(?:otp|code|auth)/i,
133
+ ];
134
+ }
135
+
136
+ async analyze(files, language, options = {}) {
137
+ const violations = [];
138
+
139
+ for (const filePath of files) {
140
+ // Skip test files, build directories, and node_modules
141
+ if (this.shouldSkipFile(filePath)) {
142
+ continue;
143
+ }
144
+
145
+ try {
146
+ const content = require('fs').readFileSync(filePath, 'utf8');
147
+ const fileViolations = this.analyzeFile(content, filePath, options);
148
+ violations.push(...fileViolations);
149
+ } catch (error) {
150
+ if (options.verbose) {
151
+ console.warn(`⚠️ Failed to analyze ${filePath}: ${error.message}`);
152
+ }
153
+ }
154
+ }
155
+
156
+ return violations;
157
+ }
158
+
159
+ shouldSkipFile(filePath) {
160
+ const skipPatterns = [
161
+ 'test/', 'tests/', '__tests__/', '.test.', '.spec.',
162
+ 'node_modules/', 'build/', 'dist/', '.next/', 'coverage/',
163
+ 'vendor/', 'mocks/', '.mock.'
164
+ // Removed 'fixtures/' to allow testing
165
+ ];
166
+
167
+ return skipPatterns.some(pattern => filePath.includes(pattern));
168
+ }
169
+
170
+ analyzeFile(content, filePath, options = {}) {
171
+ const violations = [];
172
+ const lines = content.split('\n');
173
+
174
+ lines.forEach((line, index) => {
175
+ const lineNumber = index + 1;
176
+ const trimmedLine = line.trim();
177
+
178
+ // Skip comments, imports, and empty lines
179
+ if (this.shouldSkipLine(trimmedLine)) {
180
+ return;
181
+ }
182
+
183
+ // Check for potential plaintext OTP usage
184
+ const violation = this.checkForPlaintextOtpUsage(line, lineNumber, filePath);
185
+ if (violation) {
186
+ violations.push(violation);
187
+ }
188
+ });
189
+
190
+ return violations;
191
+ }
192
+
193
+ shouldSkipLine(line) {
194
+ // Skip comments, imports, and other non-code lines
195
+ return (
196
+ line.length === 0 ||
197
+ line.startsWith('//') ||
198
+ line.startsWith('/*') ||
199
+ line.startsWith('*') ||
200
+ line.startsWith('import ') ||
201
+ line.startsWith('export ') ||
202
+ line.startsWith('require(') ||
203
+ line.includes('module.exports')
204
+ );
205
+ }
206
+
207
+ checkForPlaintextOtpUsage(line, lineNumber, filePath) {
208
+ const lowerLine = line.toLowerCase();
209
+
210
+ // First check if line contains safe patterns (early exit)
211
+ if (this.containsSafePattern(line)) {
212
+ return null;
213
+ }
214
+
215
+ // Skip lines that are clearly secure operations
216
+ if (this.isSecureOperation(line)) {
217
+ return null;
218
+ }
219
+
220
+ // Check for variable assignments with sensitive OTP names
221
+ const sensitiveAssignment = this.checkSensitiveOtpAssignment(line, lineNumber, filePath);
222
+ if (sensitiveAssignment) {
223
+ return sensitiveAssignment;
224
+ }
225
+
226
+ // Check for direct plaintext OTP patterns
227
+ for (const pattern of this.plaintextOtpPatterns) {
228
+ if (pattern.test(line)) {
229
+ // Additional context check to reduce false positives
230
+ if (this.hasOtpUsageContext(line) && !this.isSecureOperation(line)) {
231
+ return {
232
+ ruleId: this.ruleId,
233
+ severity: 'error',
234
+ message: 'OTP codes should not be stored or transmitted in plaintext. Use encrypted storage or hashed values.',
235
+ line: lineNumber,
236
+ column: this.findPatternColumn(line, pattern),
237
+ filePath: filePath,
238
+ type: 'plaintext_otp_usage',
239
+ details: 'Consider encrypting OTP codes before storage, using secure transmission channels, or storing only hashed/encrypted versions.'
240
+ };
241
+ }
242
+ }
243
+ }
244
+
245
+ return null;
246
+ }
247
+
248
+ containsSafePattern(line) {
249
+ return this.safePatterns.some(pattern => pattern.test(line));
250
+ }
251
+
252
+ isSecureOperation(line) {
253
+ // Check if the line is performing secure operations on OTP
254
+ const secureOperationPatterns = [
255
+ // Hashing operations
256
+ /(?:bcrypt\.hash|crypto\.createHash|createHmac|hash|encrypt).*(?:otp|code|auth)/i,
257
+ /(?:otp|code|auth).*(?:bcrypt\.hash|crypto\.createHash|createHmac|hash|encrypt)/i,
258
+
259
+ // Secure service calls
260
+ /(?:secure|encrypted|safe).*(?:send|email|sms|service|Email|SMS|Service)/i,
261
+
262
+ // Variable containing 'hashed', 'encrypted', 'secure', 'token'
263
+ /(?:hashed|encrypted|secure|token).*(?:otp|code|auth)/i,
264
+ /(?:otp|code|auth).*(?:hash|encrypted|secure|token)/i,
265
+
266
+ // Method calls that return tokens/hashes
267
+ /(?:create|generate).*(?:token|hash|secure)/i,
268
+
269
+ // Comparison operations (validation, not storage)
270
+ /(?:compare|verify|validate|check).*(?:otp|code|auth)/i,
271
+ /(?:otp|code|auth).*(?:compare|verify|validate|check)/i,
272
+
273
+ // Safe logging (no actual OTP values) - improved patterns
274
+ /console\.(?:log|debug|error|warn).*(?:generated|sent|success|timestamp|user|result|validation|failed)(?!.*otp.*:)/i,
275
+ /console\.error.*validation.*failed.*user/i,
276
+ /console\.error.*failed.*user.*:/i,
277
+
278
+ // Return statements with safe content
279
+ /return.*(?:timestamp|Date\.now|success|message|sent)/i,
280
+
281
+ // Simple variable declarations without immediate storage/transmission
282
+ /(?:const|let|var)\s+\w*(?:otp|code|auth)\w*\s*=\s*(?:this\.generate|generate|create|[\w.]+\()/i,
283
+
284
+ // Method calls to secure functions
285
+ /await\s+this\.(?:send|secure|encrypted|safe)/i,
286
+
287
+ // Variable assignments that are passed to secure functions
288
+ /(?:const|let|var)\s+\w+\s*=.*;\s*$|(?:const|let|var)\s+\w+\s*=.*(?:generate|create)/i,
289
+
290
+ // Crypto operations like .update() on hash/hmac objects
291
+ /\.update\(.*(?:otp|code|auth).*\+.*\)|\.update\((?:otp|code|auth).*\)/i,
292
+ /createHmac.*\.update|createHash.*\.update/i,
293
+ ];
294
+
295
+ return secureOperationPatterns.some(pattern => pattern.test(line));
296
+ }
297
+
298
+ checkSensitiveOtpAssignment(line, lineNumber, filePath) {
299
+ // Look for variable assignments that combine OTP with storage/transmission
300
+ const assignmentMatch = line.match(/(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*(.+)/);
301
+ if (!assignmentMatch) {
302
+ return null;
303
+ }
304
+
305
+ const [, variableName, valueExpr] = assignmentMatch;
306
+ const lowerVarName = variableName.toLowerCase();
307
+ const lowerValueExpr = valueExpr.toLowerCase();
308
+
309
+ // Skip safe variable names
310
+ if (this.safeVariableNames.some(pattern => pattern.test(lowerVarName))) {
311
+ return null;
312
+ }
313
+
314
+ // Check if variable name suggests OTP usage
315
+ const hasOtpKeyword = this.otpKeywords.some(keyword => {
316
+ const keywordRegex = new RegExp(keyword, 'i');
317
+ return keywordRegex.test(lowerVarName);
318
+ });
319
+
320
+ const hasStorageOrTransmissionKeyword = [
321
+ ...this.storageKeywords,
322
+ ...this.transmissionKeywords
323
+ ].some(keyword => {
324
+ const keywordRegex = new RegExp(keyword, 'i');
325
+ return keywordRegex.test(lowerVarName) || keywordRegex.test(lowerValueExpr);
326
+ });
327
+
328
+ if (hasOtpKeyword && hasStorageOrTransmissionKeyword) {
329
+ // Check if the value looks like it contains actual OTP or sensitive data
330
+ if (this.valueContainsOtp(valueExpr)) {
331
+ return {
332
+ ruleId: this.ruleId,
333
+ severity: 'warning',
334
+ message: `Variable '${variableName}' appears to handle OTP codes for storage/transmission. Ensure OTP codes are encrypted or hashed.`,
335
+ line: lineNumber,
336
+ column: line.indexOf(variableName) + 1,
337
+ filePath: filePath,
338
+ type: 'sensitive_otp_variable',
339
+ variableName: variableName,
340
+ details: 'Consider encrypting OTP codes before storage/transmission or use secure channels.'
341
+ };
342
+ }
343
+ }
344
+
345
+ return null;
346
+ }
347
+
348
+ hasOtpUsageContext(line) {
349
+ const otpUsageIndicators = [
350
+ // Storage operations
351
+ /(?:save|store|insert|update|create|persist|write|set|put|add|database|db|collection|table|redis|mongo|sql|query|orm)/i,
352
+
353
+ // Transmission operations
354
+ /(?:send|email|sms|text|message|mail|push|notify|transmit|deliver|dispatch|forward|post)/i,
355
+
356
+ // Response operations
357
+ /res\.(?:send|json|status|end)|response\.(?:send|json|status|end)|return.*(?:json|response|status)/i,
358
+
359
+ // Session/localStorage
360
+ /(?:session|localStorage|sessionStorage|cookie|cache)/i,
361
+
362
+ // Logging (often a violation when OTP is logged)
363
+ /(?:console|log|debug|trace|print)/i,
364
+
365
+ // String operations that might expose OTP
366
+ /\+.*['"`]|['"`].*\+|\$\{.*\}/,
367
+ ];
368
+
369
+ return otpUsageIndicators.some(indicator => indicator.test(line));
370
+ }
371
+
372
+ valueContainsOtp(valueExpr) {
373
+ // Check if the value expression contains actual OTP patterns or user data
374
+ const otpValuePatterns = [
375
+ // Template strings with variables
376
+ /\$\{[^}]+\}/,
377
+
378
+ // String concatenation
379
+ /\+\s*[a-zA-Z_$]/,
380
+
381
+ // Function calls that might return OTP
382
+ /\w+\([^)]*\)/,
383
+
384
+ // Property access that might be OTP
385
+ /\w+\.\w+/,
386
+
387
+ // Array/object access
388
+ /\[.*\]/,
389
+
390
+ // Direct string literals that look like OTP codes (4-8 digits/chars)
391
+ /['"`][a-zA-Z0-9]{4,8}['"`]/,
392
+
393
+ // Patterns that suggest OTP generation or user input
394
+ /(?:generate|random|user|input|request|body|params)/i,
395
+ ];
396
+
397
+ return otpValuePatterns.some(pattern => pattern.test(valueExpr));
398
+ }
399
+
400
+ findPatternColumn(line, pattern) {
401
+ const match = pattern.exec(line);
402
+ return match ? match.index + 1 : 1;
403
+ }
404
+ }
405
+
406
+ module.exports = S007Analyzer;
@@ -0,0 +1,79 @@
1
+ {
2
+ "ruleId": "S007",
3
+ "name": "No Plaintext OTP",
4
+ "description": "One-Time Passwords must not be stored in plaintext",
5
+ "category": "security",
6
+ "severity": "error",
7
+ "languages": ["typescript", "javascript", "dart", "kotlin", "java", "python", "go", "swift"],
8
+ "tags": ["security", "owasp", "cryptographic-failures", "authentication", "otp", "2fa"],
9
+ "enabled": true,
10
+ "fixable": false,
11
+ "engine": "heuristic",
12
+ "metadata": {
13
+ "owaspCategory": "A02:2021 - Cryptographic Failures",
14
+ "cweId": "CWE-256",
15
+ "description": "Storing or transmitting One-Time Passwords (OTP) in plaintext can lead to account takeover attacks. OTP codes should be encrypted during storage and transmission to prevent unauthorized access.",
16
+ "impact": "High - Account takeover, unauthorized access to 2FA protected accounts",
17
+ "likelihood": "Medium",
18
+ "remediation": "Use encrypted storage for OTP codes, hash OTP values before storage, implement secure transmission channels, and use time-limited encrypted tokens"
19
+ },
20
+ "patterns": {
21
+ "vulnerable": [
22
+ "Storing OTP codes in database without encryption",
23
+ "Transmitting OTP codes in plaintext over API responses",
24
+ "Logging OTP codes in application logs",
25
+ "Storing OTP in localStorage or sessionStorage without encryption",
26
+ "Sending OTP in email/SMS without proper encoding",
27
+ "Including OTP in URL parameters or query strings"
28
+ ],
29
+ "secure": [
30
+ "Encrypting OTP codes before database storage",
31
+ "Hashing OTP codes with salt before storage",
32
+ "Using secure channels (HTTPS) for OTP transmission",
33
+ "Implementing time-limited encrypted tokens instead of plain OTP",
34
+ "Using secure storage mechanisms for OTP",
35
+ "Masking OTP in logs and debug output"
36
+ ]
37
+ },
38
+ "examples": {
39
+ "violations": [
40
+ "const otpCode = '123456'; db.save({ userId, otpCode });",
41
+ "res.json({ success: true, otp: user.otpCode });",
42
+ "localStorage.setItem('otp', otpCode);",
43
+ "console.log('User OTP:', otpCode);",
44
+ "await sendSMS(`Your OTP is: ${otpCode}`);",
45
+ "const query = `INSERT INTO users (otp) VALUES ('${otp}')`;",
46
+ "sessionStorage.otpCode = generatedOtp;",
47
+ "return { verificationCode: otp, status: 'sent' };"
48
+ ],
49
+ "fixes": [
50
+ "const hashedOtp = await bcrypt.hash(otpCode, 10); db.save({ userId, otpCode: hashedOtp });",
51
+ "res.json({ success: true, message: 'OTP sent to registered phone' });",
52
+ "const encryptedOtp = encrypt(otpCode); localStorage.setItem('otp', encryptedOtp);",
53
+ "console.log('OTP sent successfully to user');",
54
+ "await sendEncryptedSMS(otpCode);",
55
+ "const hashedOtp = crypto.createHash('sha256').update(otp + salt).digest('hex'); const query = `INSERT INTO users (otp_hash) VALUES ('${hashedOtp}')`;",
56
+ "const encryptedOtp = encrypt(generatedOtp); sessionStorage.otpCode = encryptedOtp;",
57
+ "return { message: 'Verification code sent', status: 'sent' };"
58
+ ]
59
+ },
60
+ "configuration": {
61
+ "checkStorage": true,
62
+ "checkTransmission": true,
63
+ "checkLogging": true,
64
+ "checkResponses": true,
65
+ "checkLocalStorage": true,
66
+ "strictMode": false,
67
+ "excludePatterns": [
68
+ "test/",
69
+ "spec/",
70
+ "mock/",
71
+ "fixture/"
72
+ ]
73
+ },
74
+ "relatedRules": [
75
+ "S006",
76
+ "S012",
77
+ "S027"
78
+ ]
79
+ }