@sun-asterisk/sunlint 1.3.7 → 1.3.9

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 (51) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/config/defaults/default.json +2 -1
  3. package/config/rule-analysis-strategies.js +20 -0
  4. package/config/rules/enhanced-rules-registry.json +247 -53
  5. package/core/file-targeting-service.js +98 -7
  6. package/package.json +1 -1
  7. package/rules/common/C065_one_behavior_per_test/analyzer.js +851 -0
  8. package/rules/common/C065_one_behavior_per_test/config.json +95 -0
  9. package/rules/security/S020_no_eval_dynamic_code/README.md +136 -0
  10. package/rules/security/S020_no_eval_dynamic_code/analyzer.js +263 -0
  11. package/rules/security/S020_no_eval_dynamic_code/config.json +54 -0
  12. package/rules/security/S020_no_eval_dynamic_code/regex-based-analyzer.js +307 -0
  13. package/rules/security/S020_no_eval_dynamic_code/symbol-based-analyzer.js +280 -0
  14. package/rules/security/S024_xpath_xxe_protection/symbol-based-analyzer.js +3 -3
  15. package/rules/security/S025_server_side_validation/symbol-based-analyzer.js +3 -4
  16. package/rules/security/S030_directory_browsing_protection/README.md +128 -0
  17. package/rules/security/S030_directory_browsing_protection/analyzer.js +264 -0
  18. package/rules/security/S030_directory_browsing_protection/config.json +63 -0
  19. package/rules/security/S030_directory_browsing_protection/regex-based-analyzer.js +483 -0
  20. package/rules/security/S030_directory_browsing_protection/symbol-based-analyzer.js +539 -0
  21. package/rules/security/S033_samesite_session_cookies/symbol-based-analyzer.js +8 -9
  22. package/rules/security/S037_cache_headers/README.md +128 -0
  23. package/rules/security/S037_cache_headers/analyzer.js +263 -0
  24. package/rules/security/S037_cache_headers/config.json +50 -0
  25. package/rules/security/S037_cache_headers/regex-based-analyzer.js +463 -0
  26. package/rules/security/S037_cache_headers/symbol-based-analyzer.js +546 -0
  27. package/rules/security/S038_no_version_headers/README.md +234 -0
  28. package/rules/security/S038_no_version_headers/analyzer.js +262 -0
  29. package/rules/security/S038_no_version_headers/config.json +49 -0
  30. package/rules/security/S038_no_version_headers/regex-based-analyzer.js +339 -0
  31. package/rules/security/S038_no_version_headers/symbol-based-analyzer.js +375 -0
  32. package/rules/security/S039_no_session_tokens_in_url/README.md +198 -0
  33. package/rules/security/S039_no_session_tokens_in_url/analyzer.js +262 -0
  34. package/rules/security/S039_no_session_tokens_in_url/config.json +92 -0
  35. package/rules/security/S039_no_session_tokens_in_url/regex-based-analyzer.js +337 -0
  36. package/rules/security/S039_no_session_tokens_in_url/symbol-based-analyzer.js +443 -0
  37. package/rules/security/S049_short_validity_tokens/analyzer.js +175 -0
  38. package/rules/security/S049_short_validity_tokens/config.json +124 -0
  39. package/rules/security/S049_short_validity_tokens/regex-based-analyzer.js +295 -0
  40. package/rules/security/S049_short_validity_tokens/symbol-based-analyzer.js +389 -0
  41. package/rules/security/S051_password_length_policy/analyzer.js +410 -0
  42. package/rules/security/S051_password_length_policy/config.json +83 -0
  43. package/rules/security/S052_weak_otp_entropy/analyzer.js +403 -0
  44. package/rules/security/S052_weak_otp_entropy/config.json +57 -0
  45. package/rules/security/S054_no_default_accounts/README.md +129 -0
  46. package/rules/security/S054_no_default_accounts/analyzer.js +792 -0
  47. package/rules/security/S054_no_default_accounts/config.json +101 -0
  48. package/rules/security/S056_log_injection_protection/analyzer.js +242 -0
  49. package/rules/security/S056_log_injection_protection/config.json +148 -0
  50. package/rules/security/S056_log_injection_protection/regex-based-analyzer.js +120 -0
  51. package/rules/security/S056_log_injection_protection/symbol-based-analyzer.js +246 -0
@@ -0,0 +1,792 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { CommentDetector } = require('../../utils/rule-helpers');
4
+
5
+ /**
6
+ * S054 - Disallow Default/Built-in Accounts (admin/root/sa/...)
7
+ * Security Rule: Prevent use of default or shared accounts for better auditability and security
8
+ *
9
+ * Detects:
10
+ * - SQL/Seeder: INSERT INTO users with blocked usernames
11
+ * - Code: createUser/createAccount with blocked usernames
12
+ * - IaC: Terraform, Helm, Docker with default usernames
13
+ * - Docs: login credentials with blocked usernames
14
+ * - Config: Database/auth configs with default usernames
15
+ * - Password smells: Common weak passwords
16
+ */
17
+ class S054NoDefaultAccountsAnalyzer {
18
+ constructor(ruleIdOrOptions = 'S054', verbose = false) {
19
+ // Handle both old and new constructor signatures
20
+ if (typeof ruleIdOrOptions === 'string') {
21
+ // Old format: constructor(ruleId, verbose)
22
+ this.ruleId = ruleIdOrOptions;
23
+ this.verbose = verbose;
24
+ } else if (typeof ruleIdOrOptions === 'object' && ruleIdOrOptions !== null) {
25
+ // New format: constructor(options)
26
+ this.ruleId = 'S054'; // Use default ruleId
27
+ this.verbose = ruleIdOrOptions.verbose || false;
28
+ this.semanticEngine = ruleIdOrOptions.semanticEngine || null;
29
+ } else {
30
+ // Fallback
31
+ this.ruleId = 'S054';
32
+ this.verbose = false;
33
+ }
34
+
35
+ this.loadConfig();
36
+ }
37
+
38
+ loadConfig() {
39
+ try {
40
+ const configPath = path.join(__dirname, 'config.json');
41
+ const configContent = fs.readFileSync(configPath, 'utf8');
42
+ const config = JSON.parse(configContent);
43
+
44
+ this.blockedUsernames = config.options.blockedUsernames || [];
45
+ this.codeCreationPatterns = config.options.codeCreationPatterns || [];
46
+ this.sqlInsertUserPatterns = config.options.sqlInsertUserPatterns || [];
47
+ this.infraPatterns = config.options.infraPatterns || {};
48
+ this.docPatterns = config.options.docPatterns || [];
49
+ this.passwordSmells = config.options.passwordSmells || [];
50
+ this.configFilePatterns = config.options.configFilePatterns || [];
51
+ this.policy = config.options.policy || {};
52
+ this.allowlist = config.options.allowlist || {};
53
+ this.thresholds = config.options.thresholds || {};
54
+ this.exemptions = config.options.exemptions || {};
55
+
56
+ // Build unified regex patterns
57
+ this.blockedUsernamesRegex = new RegExp(`\\b(${this.blockedUsernames.join('|')})\\b`, 'gi');
58
+ this.passwordSmellsRegex = new RegExp(`\\b(${this.passwordSmells.join('|')})\\b`, 'gi');
59
+
60
+ if (this.verbose) {
61
+ console.log(`[DEBUG] S054: Loaded config with ${this.blockedUsernames.length} blocked usernames, ${Object.keys(this.infraPatterns).length} infra patterns`);
62
+ }
63
+ } catch (error) {
64
+ console.warn(`[S054] Failed to load config: ${error.message}`);
65
+ this.initializeDefaultConfig();
66
+ }
67
+ }
68
+
69
+ initializeDefaultConfig() {
70
+ this.blockedUsernames = ['admin', 'root', 'sa', 'test', 'guest'];
71
+ this.codeCreationPatterns = [];
72
+ this.sqlInsertUserPatterns = [];
73
+ this.infraPatterns = {};
74
+ this.docPatterns = [];
75
+ this.passwordSmells = [];
76
+ this.configFilePatterns = [];
77
+ this.policy = {};
78
+ this.allowlist = { paths: [] };
79
+ this.thresholds = {};
80
+ this.exemptions = {};
81
+ this.blockedUsernamesRegex = /\\b(admin|root|sa|test|guest)\\b/gi;
82
+ this.passwordSmellsRegex = /\\b(password|123456|admin)\\b/gi;
83
+ }
84
+
85
+ analyze(files, language, options = {}) {
86
+ this.verbose = options.verbose || false;
87
+ const violations = [];
88
+
89
+ if (this.verbose) {
90
+ console.log(`[DEBUG] 🎯 S054 ANALYZE: Starting analysis with comment detection enabled`);
91
+ console.log(`[DEBUG] 🎯 S054: Language=${language}, Files=${files.length}, Options=`, options);
92
+ }
93
+
94
+ if (!Array.isArray(files)) {
95
+ files = [files];
96
+ }
97
+
98
+ for (const filePath of files) {
99
+ if (this.verbose) {
100
+ console.log(`[DEBUG] 🎯 S054: Analyzing ${filePath.split('/').pop()}`);
101
+ }
102
+
103
+ try {
104
+ const content = fs.readFileSync(filePath, 'utf8');
105
+ const fileExtension = path.extname(filePath);
106
+ const fileName = path.basename(filePath);
107
+ const fileViolations = this.analyzeFile(filePath, content, fileExtension, fileName);
108
+ violations.push(...fileViolations);
109
+ } catch (error) {
110
+ console.warn(`[S054] Error analyzing ${filePath}: ${error.message}`);
111
+ }
112
+ }
113
+
114
+ if (this.verbose) {
115
+ console.log(`[DEBUG] 🎯 S054: Found ${violations.length} default account violations`);
116
+ }
117
+
118
+ return violations;
119
+ }
120
+
121
+ // Alias method for engines that might call this
122
+ run(filePath, content, options = {}) {
123
+ this.verbose = options.verbose || false;
124
+
125
+ if (this.verbose) {
126
+ console.log(`[DEBUG] 🎯 S054 RUN: Running comment-aware analysis on ${path.basename(filePath)}`);
127
+ }
128
+
129
+ const fileExtension = path.extname(filePath);
130
+ const fileName = path.basename(filePath);
131
+ return this.analyzeFile(filePath, content, fileExtension, fileName);
132
+ }
133
+
134
+ // Another possible entry point
135
+ runAnalysis(filePath, content, options = {}) {
136
+ return this.run(filePath, content, options);
137
+ }
138
+
139
+ // Entry point that engine might call
140
+ runEnhancedAnalysis(filePath, content, language, options = {}) {
141
+ this.verbose = options.verbose || false;
142
+
143
+ if (this.verbose) {
144
+ console.log(`[DEBUG] 🎯 S054 runEnhancedAnalysis: Running comment-aware analysis on ${path.basename(filePath)}`);
145
+ }
146
+
147
+ const fileExtension = path.extname(filePath);
148
+ const fileName = path.basename(filePath);
149
+ return this.analyzeFile(filePath, content, fileExtension, fileName);
150
+ }
151
+
152
+ analyzeFile(filePath, content, fileExtension, fileName) {
153
+ const language = this.detectLanguage(fileExtension, fileName);
154
+ if (!language) {
155
+ return [];
156
+ }
157
+
158
+ // Check if file is exempted (test files, etc.)
159
+ const isExempted = this.isExemptedFile(filePath);
160
+ if (isExempted && this.verbose) {
161
+ console.log(`[DEBUG] 🔍 S054: Analyzing exempted file: ${fileName} (will still check for password smells)`);
162
+ }
163
+
164
+ return this.analyzeWithHeuristic(filePath, content, language, isExempted);
165
+ }
166
+
167
+ detectLanguage(fileExtension, fileName) {
168
+ const extensions = {
169
+ '.sql': 'sql',
170
+ '.ts': 'typescript',
171
+ '.tsx': 'typescript',
172
+ '.js': 'javascript',
173
+ '.jsx': 'javascript',
174
+ '.mjs': 'javascript',
175
+ '.tf': 'terraform',
176
+ '.yaml': 'yaml',
177
+ '.yml': 'yaml',
178
+ '.json': 'json',
179
+ '.properties': 'properties',
180
+ '.conf': 'config',
181
+ '.config': 'config',
182
+ '.env': 'env',
183
+ '.md': 'markdown',
184
+ '.dockerfile': 'docker',
185
+ '.dockerignore': 'docker'
186
+ };
187
+
188
+ // Special file names
189
+ if (fileName.toLowerCase().includes('dockerfile')) return 'docker';
190
+ if (fileName.toLowerCase().includes('docker-compose')) return 'docker';
191
+ if (fileName.toLowerCase().includes('helm')) return 'helm';
192
+ if (fileName.toLowerCase().includes('values')) return 'helm';
193
+
194
+ return extensions[fileExtension] || 'text';
195
+ }
196
+
197
+ isExemptedFile(filePath) {
198
+ const allowedPaths = this.allowlist.paths || [];
199
+ return allowedPaths.some(path => filePath.includes(path));
200
+ }
201
+
202
+ analyzeWithHeuristic(filePath, content, language, isExempted) {
203
+ const violations = [];
204
+ const lines = content.split('\n');
205
+
206
+ if (this.verbose) {
207
+ console.log(`[DEBUG] 🎯 S054: Starting analysis of ${lines.length} lines with comment detection`);
208
+ }
209
+
210
+ let inBlockComment = false;
211
+ let skippedCommentLines = 0;
212
+
213
+ for (let i = 0; i < lines.length; i++) {
214
+ const line = lines[i];
215
+ const lineNumber = i + 1;
216
+
217
+ // Track block comments across lines
218
+ if (line.includes('/*')) {
219
+ inBlockComment = true;
220
+ if (this.verbose) {
221
+ console.log(`[DEBUG] 🎯 S054: Line ${lineNumber} starts block comment`);
222
+ }
223
+ }
224
+ if (line.includes('*/')) {
225
+ inBlockComment = false;
226
+ skippedCommentLines++;
227
+ if (this.verbose) {
228
+ console.log(`[DEBUG] 🎯 S054: Line ${lineNumber} ends block comment - skipping`);
229
+ }
230
+ continue; // Skip the closing line itself
231
+ }
232
+ if (inBlockComment) {
233
+ skippedCommentLines++;
234
+ if (this.verbose) {
235
+ console.log(`[DEBUG] 🎯 S054: Line ${lineNumber} inside block comment - skipping`);
236
+ }
237
+ continue; // Skip lines inside block comments
238
+ }
239
+
240
+ // Skip lines that are entirely single-line comments
241
+ const trimmedLine = line.trim();
242
+ if (trimmedLine.startsWith('//') || trimmedLine.startsWith('#')) {
243
+ skippedCommentLines++;
244
+ if (this.verbose) {
245
+ console.log(`[DEBUG] 🎯 S054: Line ${lineNumber} is single-line comment - skipping`);
246
+ }
247
+ continue;
248
+ }
249
+
250
+ // Use CommentDetector to clean line and get comment ranges
251
+ const { cleanLine, commentRanges } = CommentDetector.cleanLineForMatching(line);
252
+
253
+ if (this.verbose && commentRanges.length > 0) {
254
+ console.log(`[DEBUG] 🎯 S054: Line ${lineNumber} has ${commentRanges.length} comment ranges:`, commentRanges);
255
+ }
256
+
257
+ // Helper function to check if a position is in a comment
258
+ const isInComment = (position) => {
259
+ return commentRanges.some(range => position >= range.start && position < range.end);
260
+ };
261
+
262
+ // Always check for password smells (even in exempted files)
263
+ const passwordViolations = this.checkPasswordSmellsWithComments(cleanLine, lineNumber, filePath, isInComment);
264
+ if (this.verbose && passwordViolations.length > 0) {
265
+ console.log(`[DEBUG] 🎯 S054: Line ${lineNumber} password violations:`, passwordViolations.length);
266
+ }
267
+ violations.push(...passwordViolations);
268
+
269
+ // Skip other checks for exempted files unless policy requires it
270
+ if (isExempted && !this.policy.requireCheckEvenInTests) {
271
+ continue;
272
+ }
273
+
274
+ // Check based on language/file type using cleaned line
275
+ switch (language) {
276
+ case 'sql':
277
+ const sqlViolations = this.checkSQLPatternsWithComments(cleanLine, lineNumber, filePath, isInComment);
278
+ if (this.verbose && sqlViolations.length > 0) {
279
+ console.log(`[DEBUG] 🎯 S054: Line ${lineNumber} SQL violations:`, sqlViolations.length);
280
+ }
281
+ violations.push(...sqlViolations);
282
+ break;
283
+ case 'typescript':
284
+ case 'javascript':
285
+ const codeViolations = this.checkCodePatternsWithComments(cleanLine, lineNumber, filePath, isInComment);
286
+ const configViolations = this.checkConfigPatternsWithComments(cleanLine, lineNumber, filePath, isInComment);
287
+ if (this.verbose && (codeViolations.length > 0 || configViolations.length > 0)) {
288
+ console.log(`[DEBUG] 🎯 S054: Line ${lineNumber} code violations: ${codeViolations.length}, config: ${configViolations.length}`);
289
+ }
290
+ violations.push(...codeViolations);
291
+ violations.push(...configViolations);
292
+ break;
293
+ case 'terraform':
294
+ violations.push(...this.checkTerraformPatternsWithComments(cleanLine, lineNumber, filePath, isInComment));
295
+ break;
296
+ case 'yaml':
297
+ case 'helm':
298
+ violations.push(...this.checkHelmYamlPatternsWithComments(cleanLine, lineNumber, filePath, isInComment));
299
+ violations.push(...this.checkKubernetesPatternsWithComments(cleanLine, lineNumber, filePath, isInComment));
300
+ break;
301
+ case 'docker':
302
+ violations.push(...this.checkDockerPatternsWithComments(cleanLine, lineNumber, filePath, isInComment));
303
+ break;
304
+ case 'json':
305
+ case 'properties':
306
+ case 'config':
307
+ case 'env':
308
+ violations.push(...this.checkConfigPatternsWithComments(cleanLine, lineNumber, filePath, isInComment));
309
+ break;
310
+ case 'markdown':
311
+ violations.push(...this.checkDocumentationPatternsWithComments(cleanLine, lineNumber, filePath, isInComment));
312
+ break;
313
+ default:
314
+ // Generic text checks
315
+ violations.push(...this.checkGenericPatternsWithComments(cleanLine, lineNumber, filePath, isInComment));
316
+ }
317
+ }
318
+
319
+ if (this.verbose) {
320
+ console.log(`[DEBUG] 🎯 S054: Skipped ${skippedCommentLines} comment lines, found ${violations.length} violations`);
321
+ }
322
+
323
+ return violations;
324
+ }
325
+
326
+ checkPasswordSmellsWithComments(cleanLine, lineNumber, filePath, isInComment) {
327
+ const violations = [];
328
+
329
+ // Skip decorator lines (@Controller, @ApiTags, etc.)
330
+ const trimmedLine = cleanLine.trim();
331
+ if (trimmedLine.startsWith('@')) {
332
+ return violations;
333
+ }
334
+
335
+ // Skip class/interface declarations
336
+ if (trimmedLine.match(/^(class|interface|export class|export interface)\s+\w*admin\w*/i)) {
337
+ return violations;
338
+ }
339
+
340
+ // Skip import statements
341
+ if (trimmedLine.startsWith('import ') || trimmedLine.includes('from ')) {
342
+ return violations;
343
+ }
344
+
345
+ this.passwordSmells.forEach(smell => {
346
+ // More specific pattern for password detection - must be in password context
347
+ const pattern = new RegExp(`password\\s*[=:]\\s*['"]${smell}['"]|['"]${smell}['"]\\s*[=:].*password`, 'gi');
348
+ const matches = [...cleanLine.matchAll(pattern)];
349
+
350
+ // For "secret" - only flag if it's actually a hardcoded secret, not OTP/auth variables
351
+ if (smell === 'secret') {
352
+ // Skip if it's OTP/authentication related
353
+ if (cleanLine.match(/(generateSecret|hotp|totp|authenticator|otp)/i)) {
354
+ return;
355
+ }
356
+ // Skip if it's variable assignment (const secret = ...)
357
+ if (cleanLine.match(/(const|let|var)\s+secret\s*=/) || cleanLine.match(/\.secret\s*=/) || cleanLine.match(/\{\s*secret\s*\}/)) {
358
+ return;
359
+ }
360
+ }
361
+
362
+ matches.forEach(match => {
363
+ // Skip if this match is within a comment
364
+ if (isInComment(match.index)) {
365
+ return;
366
+ }
367
+
368
+ // Skip if it's just a service/method name (e.g., adminService)
369
+ if (smell === 'admin' && cleanLine.match(/\w*Service\.\w+/)) {
370
+ return;
371
+ }
372
+
373
+ violations.push({
374
+ ruleId: this.ruleId,
375
+ message: `Password smell detected: "${smell}" - use strong, unique passwords`,
376
+ severity: 'error',
377
+ line: lineNumber,
378
+ column: match.index + 1,
379
+ filePath: filePath,
380
+ context: {
381
+ violationType: 'password_smell',
382
+ evidence: match[0],
383
+ recommendation: 'Use strong, randomly generated passwords and store securely'
384
+ }
385
+ });
386
+ });
387
+ });
388
+
389
+ return violations;
390
+ }
391
+
392
+ checkCodePatternsWithComments(cleanLine, lineNumber, filePath, isInComment) {
393
+ const violations = [];
394
+
395
+ // Skip decorator lines (@Controller, @ApiTags, etc.)
396
+ const trimmedLine = cleanLine.trim();
397
+ if (trimmedLine.startsWith('@')) {
398
+ return violations;
399
+ }
400
+
401
+ // Skip import statements
402
+ if (trimmedLine.startsWith('import ') || trimmedLine.includes('from ')) {
403
+ return violations;
404
+ }
405
+
406
+ this.codeCreationPatterns.forEach(pattern => {
407
+ const regex = new RegExp(pattern, 'gi');
408
+ const match = cleanLine.match(regex);
409
+
410
+ if (match) {
411
+ // Check if the line contains blocked usernames in actual user creation context
412
+ const usernameMatches = [...cleanLine.matchAll(this.blockedUsernamesRegex)];
413
+ usernameMatches.forEach(userMatch => {
414
+ // Skip if this match is within a comment
415
+ if (isInComment(userMatch.index)) {
416
+ return;
417
+ }
418
+
419
+ const matchedTerm = userMatch[1].toLowerCase();
420
+
421
+ // Skip TypeScript/JavaScript type annotations (e.g., ): User => {, : User, <User>)
422
+ if (cleanLine.match(/:\s*User\s*(=>|\s*[,\)\{\}]|$)/) && matchedTerm === 'user') {
423
+ return;
424
+ }
425
+
426
+ // Skip function parameters with types (e.g., (user: User))
427
+ if (cleanLine.match(/\(\s*\w*\s*:\s*User\s*\)/) && matchedTerm === 'user') {
428
+ return;
429
+ }
430
+
431
+ // Skip variable declarations with types (e.g., const user: User)
432
+ if (cleanLine.match(/(const|let|var)\s+\w+\s*:\s*User/) && matchedTerm === 'user') {
433
+ return;
434
+ }
435
+
436
+ // Skip generic type usage (e.g., Array<User>, Promise<User>)
437
+ if (cleanLine.match(/<\s*User\s*>/) && matchedTerm === 'user') {
438
+ return;
439
+ }
440
+
441
+ // Skip API documentation examples (e.g., example: 'admin', example: 'example')
442
+ if (cleanLine.match(/example\s*:\s*['"]/)) {
443
+ return;
444
+ }
445
+
446
+ // Skip schema/swagger documentation
447
+ if (cleanLine.match(/(type|format|nullable|properties)\s*:/)) {
448
+ return;
449
+ }
450
+
451
+ // Skip if it's destructuring from request object (e.g., { user })
452
+ if (cleanLine.match(/[{,]\s*user\s*[,}]/) && matchedTerm === 'user') {
453
+ return;
454
+ }
455
+
456
+ // Skip if it's object property access (e.g., user.userName, user.property, admin.userName)
457
+ if (cleanLine.match(/\b(user|admin|root|guest|test)\.[a-zA-Z_$][a-zA-Z0-9_$]*/) && ['user', 'admin', 'root', 'guest', 'test'].includes(matchedTerm)) {
458
+ return;
459
+ }
460
+
461
+ // Skip if it's property assignment from object (e.g., userName: user.userName)
462
+ if (cleanLine.match(/\w+\s*:\s*(user|admin|root|guest|test)\.[a-zA-Z_$][a-zA-Z0-9_$]*/) && ['user', 'admin', 'root', 'guest', 'test'].includes(matchedTerm)) {
463
+ return;
464
+ }
465
+
466
+ // Skip if it's variable assignment from object (e.g., const userName = user.userName)
467
+ if (cleanLine.match(/(const|let|var)\s+\w+\s*=\s*(user|admin|root|guest|test)\.[a-zA-Z_$][a-zA-Z0-9_$]*/) && ['user', 'admin', 'root', 'guest', 'test'].includes(matchedTerm)) {
468
+ return;
469
+ }
470
+
471
+ // Skip property assignment inside object literal (e.g., { userName: userData.uniqueUsername })
472
+ if (cleanLine.match(/\{\s*\w+\s*:\s*\w+\.\w+/) && matchedTerm === 'user') {
473
+ return;
474
+ }
475
+
476
+ // Skip return statement with object literal
477
+ if (cleanLine.match(/return\s*\{[\s\w:,\.]*\}/) && matchedTerm === 'user') {
478
+ return;
479
+ }
480
+
481
+ // Skip method visibility modifiers
482
+ if (cleanLine.match(/\b(public|private|protected)\s/) && matchedTerm === 'public') {
483
+ return;
484
+ }
485
+
486
+ // Skip if it's a service method call (e.g., adminService.method)
487
+ if (cleanLine.match(/\w*Service\.\w+/) && ['admin', 'user'].includes(matchedTerm)) {
488
+ return;
489
+ }
490
+
491
+ // Skip if it's in route path or controller name
492
+ if (trimmedLine.includes('@Controller') || trimmedLine.includes('@ApiTags')) {
493
+ return;
494
+ }
495
+
496
+ // Skip interface/class definitions
497
+ if (cleanLine.match(/(interface|class|type)\s+\w*User\w*/) && matchedTerm === 'user') {
498
+ return;
499
+ }
500
+
501
+ violations.push({
502
+ ruleId: this.ruleId,
503
+ message: `Default account "${userMatch[1]}" found in user creation code - use named accounts`,
504
+ severity: 'error',
505
+ line: lineNumber,
506
+ column: userMatch.index + 1,
507
+ filePath: filePath,
508
+ context: {
509
+ violationType: 'code_default_account',
510
+ evidence: cleanLine.trim(),
511
+ recommendation: 'Implement proper user registration with unique usernames'
512
+ }
513
+ });
514
+ });
515
+ }
516
+ });
517
+
518
+ return violations;
519
+ }
520
+
521
+ checkSQLPatternsWithComments(cleanLine, lineNumber, filePath, isInComment) {
522
+ const violations = [];
523
+
524
+ this.sqlInsertUserPatterns.forEach(pattern => {
525
+ const regex = new RegExp(pattern, 'gi');
526
+ const match = cleanLine.match(regex);
527
+
528
+ if (match) {
529
+ // Check if the line contains blocked usernames
530
+ const usernameMatches = [...cleanLine.matchAll(this.blockedUsernamesRegex)];
531
+ usernameMatches.forEach(userMatch => {
532
+ // Skip if this match is within a comment
533
+ if (isInComment(userMatch.index)) {
534
+ return;
535
+ }
536
+
537
+ violations.push({
538
+ ruleId: this.ruleId,
539
+ message: `Default account "${userMatch[1]}" found in SQL statement - use named user accounts`,
540
+ severity: 'error',
541
+ line: lineNumber,
542
+ column: userMatch.index + 1,
543
+ filePath: filePath,
544
+ context: {
545
+ violationType: 'sql_default_account',
546
+ evidence: cleanLine.trim(),
547
+ recommendation: 'Create individual user accounts with appropriate permissions'
548
+
549
+ }
550
+ });
551
+ });
552
+ }
553
+ });
554
+
555
+ return violations;
556
+ }
557
+
558
+ checkTerraformPatternsWithComments(cleanLine, lineNumber, filePath, isInComment) {
559
+ const violations = [];
560
+
561
+ (this.infraPatterns.terraform || []).forEach(pattern => {
562
+ const regex = new RegExp(pattern, 'gi');
563
+ const matches = [...cleanLine.matchAll(regex)];
564
+
565
+ matches.forEach(match => {
566
+ // Skip if this match is within a comment
567
+ if (isInComment(match.index)) {
568
+ return;
569
+ }
570
+
571
+ violations.push({
572
+ ruleId: this.ruleId,
573
+ message: `Default account "${match[1] || 'detected'}" found in Terraform config - use named accounts`,
574
+ severity: 'error',
575
+ line: lineNumber,
576
+ column: match.index + 1,
577
+ filePath: filePath,
578
+ context: {
579
+ violationType: 'terraform_default_account',
580
+ evidence: cleanLine.trim(),
581
+ recommendation: 'Use variables or data sources for user account names'
582
+
583
+ }
584
+ });
585
+ });
586
+ });
587
+
588
+ return violations;
589
+ }
590
+
591
+ checkHelmYamlPatternsWithComments(cleanLine, lineNumber, filePath, isInComment) {
592
+ const violations = [];
593
+
594
+ (this.infraPatterns.helmValues || []).forEach(pattern => {
595
+ const regex = new RegExp(pattern, 'gi');
596
+ const matches = [...cleanLine.matchAll(regex)];
597
+
598
+ matches.forEach(match => {
599
+ // Skip if this match is within a comment
600
+ if (isInComment(match.index)) {
601
+ return;
602
+ }
603
+
604
+ violations.push({
605
+ ruleId: this.ruleId,
606
+ message: `Default account configuration "${match[0]}" found in Helm values - use configurable accounts`,
607
+ severity: 'error',
608
+ line: lineNumber,
609
+ column: match.index + 1,
610
+ filePath: filePath,
611
+ context: {
612
+ violationType: 'helm_default_account',
613
+ evidence: cleanLine.trim(),
614
+ recommendation: 'Make user accounts configurable through values.yaml'
615
+
616
+ }
617
+ });
618
+ });
619
+ });
620
+
621
+ return violations;
622
+ }
623
+
624
+ checkKubernetesPatternsWithComments(cleanLine, lineNumber, filePath, isInComment) {
625
+ const violations = [];
626
+
627
+ (this.infraPatterns.kubernetes || []).forEach(pattern => {
628
+ const regex = new RegExp(pattern, 'gi');
629
+ const matches = [...cleanLine.matchAll(regex)];
630
+
631
+ matches.forEach(match => {
632
+ // Skip if this match is within a comment
633
+ if (isInComment(match.index)) {
634
+ return;
635
+ }
636
+
637
+ violations.push({
638
+ ruleId: this.ruleId,
639
+ message: `Default Kubernetes account configuration found - avoid using default accounts`,
640
+ severity: 'error',
641
+ line: lineNumber,
642
+ column: match.index + 1,
643
+ filePath: filePath,
644
+ context: {
645
+ violationType: 'kubernetes_default_account',
646
+ evidence: cleanLine.trim(),
647
+ recommendation: 'Create dedicated service accounts with minimal required permissions'
648
+
649
+ }
650
+ });
651
+ });
652
+ });
653
+
654
+ return violations;
655
+ }
656
+
657
+ checkDockerPatternsWithComments(cleanLine, lineNumber, filePath, isInComment) {
658
+ const violations = [];
659
+
660
+ (this.infraPatterns.docker || []).forEach(pattern => {
661
+ const regex = new RegExp(pattern, 'gi');
662
+ const matches = [...cleanLine.matchAll(regex)];
663
+
664
+ matches.forEach(match => {
665
+ // Skip if this match is within a comment
666
+ if (isInComment(match.index)) {
667
+ return;
668
+ }
669
+
670
+ violations.push({
671
+ ruleId: this.ruleId,
672
+ message: `Default account in Docker configuration - use named accounts`,
673
+ severity: 'error',
674
+ line: lineNumber,
675
+ column: match.index + 1,
676
+ filePath: filePath,
677
+ context: {
678
+ violationType: 'docker_default_account',
679
+ evidence: cleanLine.trim(),
680
+ recommendation: 'Use environment variables for database credentials'
681
+
682
+ }
683
+ });
684
+ });
685
+ });
686
+
687
+ return violations;
688
+ }
689
+
690
+ checkConfigPatternsWithComments(cleanLine, lineNumber, filePath, isInComment) {
691
+ const violations = [];
692
+
693
+ this.configFilePatterns.forEach(pattern => {
694
+ const regex = new RegExp(pattern, 'gi');
695
+ const matches = [...cleanLine.matchAll(regex)];
696
+
697
+ matches.forEach(match => {
698
+ // Skip if this match is within a comment
699
+ if (isInComment(match.index)) {
700
+ return;
701
+ }
702
+
703
+ violations.push({
704
+ ruleId: this.ruleId,
705
+ message: `Default account in configuration file - use named accounts`,
706
+ severity: 'error',
707
+ line: lineNumber,
708
+ column: match.index + 1,
709
+ filePath: filePath,
710
+ context: {
711
+ violationType: 'config_default_account',
712
+ evidence: cleanLine.trim(),
713
+ recommendation: 'Use environment variables or secure configuration management'
714
+
715
+ }
716
+ });
717
+ });
718
+ });
719
+
720
+ return violations;
721
+ }
722
+
723
+ checkDocumentationPatternsWithComments(cleanLine, lineNumber, filePath, isInComment) {
724
+ const violations = [];
725
+
726
+ this.docPatterns.forEach(pattern => {
727
+ const regex = new RegExp(pattern, 'gi');
728
+ const matches = [...cleanLine.matchAll(regex)];
729
+
730
+ matches.forEach(match => {
731
+ // Skip if this match is within a comment
732
+ if (isInComment(match.index)) {
733
+ return;
734
+ }
735
+
736
+ violations.push({
737
+ ruleId: this.ruleId,
738
+ message: `Default credentials in documentation - avoid exposing default accounts`,
739
+ severity: 'warning',
740
+ line: lineNumber,
741
+ column: match.index + 1,
742
+ filePath: filePath,
743
+ context: {
744
+ violationType: 'doc_default_account',
745
+ evidence: cleanLine.trim(),
746
+ recommendation: 'Use placeholder examples like "your-username" or "example-user"'
747
+
748
+ }
749
+ });
750
+ });
751
+ });
752
+
753
+ return violations;
754
+ }
755
+
756
+ checkGenericPatternsWithComments(cleanLine, lineNumber, filePath, isInComment) {
757
+ const violations = [];
758
+
759
+ // Generic check for blocked usernames in any context
760
+ const usernameMatches = [...cleanLine.matchAll(this.blockedUsernamesRegex)];
761
+ usernameMatches.forEach(match => {
762
+ // Skip if this match is within a comment
763
+ if (isInComment(match.index)) {
764
+ return;
765
+ }
766
+
767
+ // Skip if it's just a comment or documentation
768
+ if (cleanLine.trim().startsWith('//') || cleanLine.trim().startsWith('#') || cleanLine.trim().startsWith('*')) {
769
+ return;
770
+ }
771
+
772
+ violations.push({
773
+ ruleId: this.ruleId,
774
+ message: `Potential default account "${match[1]}" detected - verify this is intentional`,
775
+ severity: 'warning',
776
+ line: lineNumber,
777
+ column: match.index + 1,
778
+ filePath: filePath,
779
+ context: {
780
+ violationType: 'generic_default_account',
781
+ evidence: cleanLine.trim(),
782
+ recommendation: 'Use named user accounts or verify this usage is appropriate'
783
+
784
+ }
785
+ });
786
+ });
787
+
788
+ return violations;
789
+ }
790
+ }
791
+
792
+ module.exports = S054NoDefaultAccountsAnalyzer;