@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.
- package/CHANGELOG.md +63 -0
- package/config/defaults/default.json +2 -1
- package/config/rule-analysis-strategies.js +20 -0
- package/config/rules/enhanced-rules-registry.json +247 -53
- package/core/file-targeting-service.js +98 -7
- package/package.json +1 -1
- package/rules/common/C065_one_behavior_per_test/analyzer.js +851 -0
- package/rules/common/C065_one_behavior_per_test/config.json +95 -0
- package/rules/security/S020_no_eval_dynamic_code/README.md +136 -0
- package/rules/security/S020_no_eval_dynamic_code/analyzer.js +263 -0
- package/rules/security/S020_no_eval_dynamic_code/config.json +54 -0
- package/rules/security/S020_no_eval_dynamic_code/regex-based-analyzer.js +307 -0
- package/rules/security/S020_no_eval_dynamic_code/symbol-based-analyzer.js +280 -0
- package/rules/security/S024_xpath_xxe_protection/symbol-based-analyzer.js +3 -3
- package/rules/security/S025_server_side_validation/symbol-based-analyzer.js +3 -4
- package/rules/security/S030_directory_browsing_protection/README.md +128 -0
- package/rules/security/S030_directory_browsing_protection/analyzer.js +264 -0
- package/rules/security/S030_directory_browsing_protection/config.json +63 -0
- package/rules/security/S030_directory_browsing_protection/regex-based-analyzer.js +483 -0
- package/rules/security/S030_directory_browsing_protection/symbol-based-analyzer.js +539 -0
- package/rules/security/S033_samesite_session_cookies/symbol-based-analyzer.js +8 -9
- package/rules/security/S037_cache_headers/README.md +128 -0
- package/rules/security/S037_cache_headers/analyzer.js +263 -0
- package/rules/security/S037_cache_headers/config.json +50 -0
- package/rules/security/S037_cache_headers/regex-based-analyzer.js +463 -0
- package/rules/security/S037_cache_headers/symbol-based-analyzer.js +546 -0
- package/rules/security/S038_no_version_headers/README.md +234 -0
- package/rules/security/S038_no_version_headers/analyzer.js +262 -0
- package/rules/security/S038_no_version_headers/config.json +49 -0
- package/rules/security/S038_no_version_headers/regex-based-analyzer.js +339 -0
- package/rules/security/S038_no_version_headers/symbol-based-analyzer.js +375 -0
- package/rules/security/S039_no_session_tokens_in_url/README.md +198 -0
- package/rules/security/S039_no_session_tokens_in_url/analyzer.js +262 -0
- package/rules/security/S039_no_session_tokens_in_url/config.json +92 -0
- package/rules/security/S039_no_session_tokens_in_url/regex-based-analyzer.js +337 -0
- package/rules/security/S039_no_session_tokens_in_url/symbol-based-analyzer.js +443 -0
- package/rules/security/S049_short_validity_tokens/analyzer.js +175 -0
- package/rules/security/S049_short_validity_tokens/config.json +124 -0
- package/rules/security/S049_short_validity_tokens/regex-based-analyzer.js +295 -0
- package/rules/security/S049_short_validity_tokens/symbol-based-analyzer.js +389 -0
- package/rules/security/S051_password_length_policy/analyzer.js +410 -0
- package/rules/security/S051_password_length_policy/config.json +83 -0
- package/rules/security/S052_weak_otp_entropy/analyzer.js +403 -0
- package/rules/security/S052_weak_otp_entropy/config.json +57 -0
- package/rules/security/S054_no_default_accounts/README.md +129 -0
- package/rules/security/S054_no_default_accounts/analyzer.js +792 -0
- package/rules/security/S054_no_default_accounts/config.json +101 -0
- package/rules/security/S056_log_injection_protection/analyzer.js +242 -0
- package/rules/security/S056_log_injection_protection/config.json +148 -0
- package/rules/security/S056_log_injection_protection/regex-based-analyzer.js +120 -0
- 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;
|