@sun-asterisk/sunlint 1.3.7 → 1.3.8
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 +38 -0
- package/config/defaults/default.json +2 -1
- package/config/rule-analysis-strategies.js +20 -0
- package/config/rules/enhanced-rules-registry.json +190 -35
- package/core/file-targeting-service.js +83 -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/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 +436 -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 +287 -0
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* S052: OTP must have ≥20-bit entropy (≥6 digits) and use CSPRNG
|
|
6
|
+
*
|
|
7
|
+
* Detects:
|
|
8
|
+
* 1. Weak RNG APIs (Math.random, Random()) in OTP context
|
|
9
|
+
* 2. Short OTP codes (4 digits)
|
|
10
|
+
* 3. Non-CSPRNG usage in OTP generation
|
|
11
|
+
* 4. Policy violations (logging OTP, no TTL, etc.)
|
|
12
|
+
*
|
|
13
|
+
* Uses hybrid approach: AST for context-aware detection + regex for patterns
|
|
14
|
+
*/
|
|
15
|
+
class S052WeakOtpEntropyAnalyzer {
|
|
16
|
+
constructor(config = null) {
|
|
17
|
+
this.ruleId = 'S052';
|
|
18
|
+
this.loadConfig(config);
|
|
19
|
+
|
|
20
|
+
// Compile regex patterns for performance
|
|
21
|
+
this.compiledPatterns = this.compilePatterns();
|
|
22
|
+
this.verbose = false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
loadConfig(config) {
|
|
26
|
+
try {
|
|
27
|
+
if (config && config.options) {
|
|
28
|
+
this.config = config;
|
|
29
|
+
this.otpIdentifiers = config.options.otpIdentifiers || [];
|
|
30
|
+
this.bannedRngApis = config.options.bannedRngApis || {};
|
|
31
|
+
this.allowedRngApis = config.options.allowedRngApis || {};
|
|
32
|
+
this.lengthChecks = config.options.lengthChecks || {};
|
|
33
|
+
this.policy = config.options.policy || {};
|
|
34
|
+
this.detectionHeuristics = config.options.detectionHeuristics || {};
|
|
35
|
+
this.allowlist = config.options.allowlist || { paths: [] };
|
|
36
|
+
this.thresholds = config.options.thresholds || {};
|
|
37
|
+
} else {
|
|
38
|
+
// Load from config file
|
|
39
|
+
const configPath = path.join(__dirname, 'config.json');
|
|
40
|
+
const configData = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
41
|
+
this.loadConfig(configData);
|
|
42
|
+
}
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.warn(`[S052] Failed to load config: ${error.message}`);
|
|
45
|
+
this.initializeDefaultConfig();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
initializeDefaultConfig() {
|
|
50
|
+
this.otpIdentifiers = ['otp', 'oneTime', 'passcode', 'verificationCode', 'pin'];
|
|
51
|
+
this.bannedRngApis = {
|
|
52
|
+
typescript: ['Math\\.random\\s*\\('],
|
|
53
|
+
javascript: ['Math\\.random\\s*\\(']
|
|
54
|
+
};
|
|
55
|
+
this.allowedRngApis = {
|
|
56
|
+
node: ['crypto\\.randomInt\\s*\\(', 'crypto\\.randomBytes\\s*\\(']
|
|
57
|
+
};
|
|
58
|
+
this.lengthChecks = {
|
|
59
|
+
numericMinDigits: 6,
|
|
60
|
+
regexBadNumeric4: '\\\\b\\\\d{4}\\\\b'
|
|
61
|
+
};
|
|
62
|
+
this.policy = {
|
|
63
|
+
requireCsprng: true,
|
|
64
|
+
forbidNonCryptoRng: true,
|
|
65
|
+
forbidFourDigitOtp: true
|
|
66
|
+
};
|
|
67
|
+
this.detectionHeuristics = {
|
|
68
|
+
variableNameMatchBoost: true,
|
|
69
|
+
builderFunctions: ['generateOtp', 'issueOtp', 'createCode']
|
|
70
|
+
};
|
|
71
|
+
this.allowlist = { paths: ['test/', 'tests/', '__tests__/'] };
|
|
72
|
+
this.thresholds = { maxBannedRngUsages: 0, maxShortOtpPatterns: 0 };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
compilePatterns() {
|
|
76
|
+
const patterns = {
|
|
77
|
+
bannedRng: {},
|
|
78
|
+
allowedRng: {},
|
|
79
|
+
otpContext: new RegExp(`\\b(${this.otpIdentifiers.join('|')})\\b`, 'gi'),
|
|
80
|
+
fourDigitOtp: /\b\d{4}\b/g,
|
|
81
|
+
shortAlphanumeric: /\b[a-zA-Z0-9]{1,5}\b/g,
|
|
82
|
+
builderFunctions: new RegExp(`\\b(${this.detectionHeuristics.builderFunctions?.join('|') || 'generateOtp'})\\s*\\(`, 'gi')
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// Compile banned RNG patterns by language
|
|
86
|
+
for (const [lang, regexes] of Object.entries(this.bannedRngApis)) {
|
|
87
|
+
patterns.bannedRng[lang] = regexes.map(regex => new RegExp(regex, 'gi'));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Compile allowed RNG patterns by language
|
|
91
|
+
for (const [lang, regexes] of Object.entries(this.allowedRngApis)) {
|
|
92
|
+
patterns.allowedRng[lang] = regexes.map(regex => new RegExp(regex, 'gi'));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return patterns;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
analyze(files, language, options = {}) {
|
|
99
|
+
this.verbose = options.verbose || false;
|
|
100
|
+
const violations = [];
|
|
101
|
+
|
|
102
|
+
if (this.verbose) {
|
|
103
|
+
console.log(`[DEBUG] 🎯 S052 ANALYZE: Starting OTP entropy analysis`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!Array.isArray(files)) {
|
|
107
|
+
files = [files];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
for (const filePath of files) {
|
|
111
|
+
if (this.verbose) {
|
|
112
|
+
console.log(`[DEBUG] 🎯 S052: Analyzing ${filePath.split('/').pop()}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
117
|
+
const fileExtension = path.extname(filePath);
|
|
118
|
+
const fileName = path.basename(filePath);
|
|
119
|
+
const fileViolations = this.analyzeFile(filePath, content, fileExtension, fileName);
|
|
120
|
+
violations.push(...fileViolations);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
console.warn(`[S052] Error analyzing ${filePath}: ${error.message}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (this.verbose) {
|
|
127
|
+
console.log(`[DEBUG] 🎯 S052: Found ${violations.length} OTP entropy violations`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return violations;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Alias methods for different engines
|
|
134
|
+
run(filePath, content, options = {}) {
|
|
135
|
+
this.verbose = options.verbose || false;
|
|
136
|
+
const fileExtension = path.extname(filePath);
|
|
137
|
+
const fileName = path.basename(filePath);
|
|
138
|
+
return this.analyzeFile(filePath, content, fileExtension, fileName);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
runAnalysis(filePath, content, options = {}) {
|
|
142
|
+
return this.run(filePath, content, options);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
runEnhancedAnalysis(filePath, content, language, options = {}) {
|
|
146
|
+
return this.run(filePath, content, options);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
analyzeFile(filePath, content, fileExtension, fileName) {
|
|
150
|
+
const language = this.detectLanguage(fileExtension, fileName);
|
|
151
|
+
if (!language) {
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Check if file is exempted
|
|
156
|
+
const isExempted = this.isExemptedFile(filePath);
|
|
157
|
+
if (isExempted && this.verbose) {
|
|
158
|
+
console.log(`[DEBUG] 🔍 S052: Analyzing exempted file: ${fileName}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return this.analyzeWithHybridApproach(filePath, content, language, isExempted);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
detectLanguage(fileExtension, fileName) {
|
|
165
|
+
const extensions = {
|
|
166
|
+
'.ts': 'typescript',
|
|
167
|
+
'.tsx': 'typescript',
|
|
168
|
+
'.js': 'javascript',
|
|
169
|
+
'.jsx': 'javascript',
|
|
170
|
+
'.mjs': 'javascript',
|
|
171
|
+
'.java': 'java',
|
|
172
|
+
'.kt': 'kotlin',
|
|
173
|
+
'.dart': 'dart'
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
return extensions[fileExtension] || null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
isExemptedFile(filePath) {
|
|
180
|
+
const allowedPaths = this.allowlist.paths || [];
|
|
181
|
+
return allowedPaths.some(path => filePath.includes(path));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
analyzeWithHybridApproach(filePath, content, language, isExempted) {
|
|
185
|
+
const violations = [];
|
|
186
|
+
const lines = content.split('\n');
|
|
187
|
+
|
|
188
|
+
if (this.verbose) {
|
|
189
|
+
console.log(`[DEBUG] 🎯 S052: Starting hybrid analysis (AST + regex) of ${lines.length} lines`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Step 1: Quick regex scan for obvious patterns
|
|
193
|
+
const regexViolations = this.scanWithRegex(content, lines, filePath, language, isExempted);
|
|
194
|
+
violations.push(...regexViolations);
|
|
195
|
+
|
|
196
|
+
// Step 2: Context-aware AST analysis (for TypeScript/JavaScript)
|
|
197
|
+
if (['typescript', 'javascript'].includes(language)) {
|
|
198
|
+
const astViolations = this.scanWithAst(content, lines, filePath, isExempted);
|
|
199
|
+
violations.push(...astViolations);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (this.verbose) {
|
|
203
|
+
console.log(`[DEBUG] 🎯 S052: Found ${violations.length} total violations (regex + AST)`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return violations;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
scanWithRegex(content, lines, filePath, language, isExempted) {
|
|
210
|
+
const violations = [];
|
|
211
|
+
|
|
212
|
+
// 1. Check for banned RNG APIs
|
|
213
|
+
if (this.policy.forbidNonCryptoRng && this.compiledPatterns.bannedRng[language]) {
|
|
214
|
+
for (const pattern of this.compiledPatterns.bannedRng[language]) {
|
|
215
|
+
const matches = [...content.matchAll(pattern)];
|
|
216
|
+
for (const match of matches) {
|
|
217
|
+
const lineNumber = this.getLineNumber(content, match.index);
|
|
218
|
+
|
|
219
|
+
// Check if in OTP context
|
|
220
|
+
const lineContent = lines[lineNumber - 1];
|
|
221
|
+
const isOtpContext = this.isInOtpContext(lineContent, lines, lineNumber);
|
|
222
|
+
|
|
223
|
+
if (isOtpContext) {
|
|
224
|
+
violations.push({
|
|
225
|
+
ruleId: this.ruleId,
|
|
226
|
+
message: `Weak RNG "${match[0]}" detected in OTP context - use CSPRNG instead`,
|
|
227
|
+
severity: 'error',
|
|
228
|
+
line: lineNumber,
|
|
229
|
+
column: this.getColumnNumber(content, match.index),
|
|
230
|
+
filePath: filePath,
|
|
231
|
+
context: {
|
|
232
|
+
violationType: 'weak_rng_in_otp',
|
|
233
|
+
evidence: lineContent.trim(),
|
|
234
|
+
recommendation: 'Use crypto.randomInt() or crypto.randomBytes() for OTP generation'
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// 2. Check for 4-digit OTP patterns
|
|
243
|
+
if (this.policy.forbidFourDigitOtp) {
|
|
244
|
+
const fourDigitMatches = [...content.matchAll(this.compiledPatterns.fourDigitOtp)];
|
|
245
|
+
for (const match of fourDigitMatches) {
|
|
246
|
+
const lineNumber = this.getLineNumber(content, match.index);
|
|
247
|
+
const lineContent = lines[lineNumber - 1];
|
|
248
|
+
|
|
249
|
+
// Check if this looks like an OTP (not just any 4 digits)
|
|
250
|
+
const isOtpContext = this.isInOtpContext(lineContent, lines, lineNumber);
|
|
251
|
+
const looks4DigitOtp = /\b(otp|code|pin).*\d{4}|\d{4}.*(otp|code|pin)/i.test(lineContent);
|
|
252
|
+
|
|
253
|
+
if (isOtpContext && looks4DigitOtp) {
|
|
254
|
+
violations.push({
|
|
255
|
+
ruleId: this.ruleId,
|
|
256
|
+
message: `4-digit OTP detected - insufficient entropy, use ≥6 digits`,
|
|
257
|
+
severity: 'error',
|
|
258
|
+
line: lineNumber,
|
|
259
|
+
column: this.getColumnNumber(content, match.index),
|
|
260
|
+
filePath: filePath,
|
|
261
|
+
context: {
|
|
262
|
+
violationType: 'insufficient_otp_entropy',
|
|
263
|
+
evidence: lineContent.trim(),
|
|
264
|
+
recommendation: 'Use at least 6 digits for OTP to achieve ≥20-bit entropy'
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return violations;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
scanWithAst(content, lines, filePath, isExempted) {
|
|
275
|
+
// AST analysis for TypeScript/JavaScript
|
|
276
|
+
// This would be more sophisticated context-aware detection
|
|
277
|
+
const violations = [];
|
|
278
|
+
|
|
279
|
+
// For now, implement simple variable/function name based detection
|
|
280
|
+
// TODO: Integrate with ts-morph for full AST analysis if needed
|
|
281
|
+
|
|
282
|
+
for (let i = 0; i < lines.length; i++) {
|
|
283
|
+
const line = lines[i];
|
|
284
|
+
const lineNumber = i + 1;
|
|
285
|
+
|
|
286
|
+
// Check for OTP generation functions with weak patterns
|
|
287
|
+
const otpFunctionMatch = line.match(/function\s+(\w*(?:otp|code|pin)\w*)\s*\(/i);
|
|
288
|
+
if (otpFunctionMatch) {
|
|
289
|
+
// Look ahead for Math.random usage in function body
|
|
290
|
+
const functionBody = this.extractFunctionBody(lines, i);
|
|
291
|
+
if (functionBody && /Math\.random\s*\(/.test(functionBody)) {
|
|
292
|
+
violations.push({
|
|
293
|
+
ruleId: this.ruleId,
|
|
294
|
+
message: `OTP generation function "${otpFunctionMatch[1]}" uses weak Math.random()`,
|
|
295
|
+
severity: 'error',
|
|
296
|
+
line: lineNumber,
|
|
297
|
+
column: otpFunctionMatch.index + 1,
|
|
298
|
+
filePath: filePath,
|
|
299
|
+
context: {
|
|
300
|
+
violationType: 'weak_rng_in_otp_function',
|
|
301
|
+
evidence: line.trim(),
|
|
302
|
+
recommendation: 'Replace Math.random() with crypto.randomInt() or crypto.randomBytes()'
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return violations;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
isInOtpContext(lineContent, lines, lineNumber) {
|
|
313
|
+
// Skip if it's clearly not OTP context (e.g., generateSessionId, generateRequestId)
|
|
314
|
+
if (/generate(Session|Request|Unique|Random)Id/i.test(lineContent)) {
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Skip if it's in a comment
|
|
319
|
+
if (/^\s*\/\//.test(lineContent.trim()) || /\/\*.*\*\//.test(lineContent)) {
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Skip if it's dummy/test data (static values in objects)
|
|
324
|
+
if (/:\s*['"`]\d+['"`]\s*[,}]/.test(lineContent)) {
|
|
325
|
+
return false; // Static hardcoded values like pin: '1000'
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Skip if it's object property assignment with literal values
|
|
329
|
+
if (/\w+\s*:\s*['"`]\w*['"`]/.test(lineContent)) {
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Check current line for direct OTP context (must be variable assignment or function call)
|
|
334
|
+
const directOtpMatch = /\b(otp|oneTime|one_time|passcode|verificationCode|verifyCode|confirmCode|pin|totp|hotp|resetCode|activationCode)\s*[:=]/.test(lineContent);
|
|
335
|
+
if (directOtpMatch && !/:\s*['"`]/.test(lineContent)) {
|
|
336
|
+
// Only if it's NOT a static value assignment
|
|
337
|
+
return true;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Check for OTP function definition
|
|
341
|
+
const otpFunctionMatch = /function\s+\w*(otp|passcode|pin|code|verify|confirm|reset|activation)\w*/i.test(lineContent);
|
|
342
|
+
if (otpFunctionMatch) {
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Check surrounding lines for context (more restrictive)
|
|
347
|
+
const contextRange = 2; // Reduced from 3
|
|
348
|
+
const start = Math.max(0, lineNumber - contextRange - 1);
|
|
349
|
+
const end = Math.min(lines.length, lineNumber + contextRange);
|
|
350
|
+
|
|
351
|
+
for (let i = start; i < end; i++) {
|
|
352
|
+
const contextLine = lines[i];
|
|
353
|
+
// Look for actual OTP assignment or return statements (not static values)
|
|
354
|
+
if (/\b(otp|passcode|pin)\s*[:=]|\breturn\s+\w*(otp|passcode|pin)/i.test(contextLine) &&
|
|
355
|
+
!/:\s*['"`]\w*['"`]/.test(contextLine)) {
|
|
356
|
+
return true;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
extractFunctionBody(lines, startLine) {
|
|
364
|
+
// Simple function body extraction (could be improved with proper parsing)
|
|
365
|
+
let braceCount = 0;
|
|
366
|
+
let inFunction = false;
|
|
367
|
+
let body = '';
|
|
368
|
+
|
|
369
|
+
for (let i = startLine; i < Math.min(lines.length, startLine + 20); i++) {
|
|
370
|
+
const line = lines[i];
|
|
371
|
+
|
|
372
|
+
for (const char of line) {
|
|
373
|
+
if (char === '{') {
|
|
374
|
+
braceCount++;
|
|
375
|
+
inFunction = true;
|
|
376
|
+
} else if (char === '}') {
|
|
377
|
+
braceCount--;
|
|
378
|
+
if (braceCount === 0 && inFunction) {
|
|
379
|
+
return body;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (inFunction) {
|
|
385
|
+
body += line + '\n';
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return body;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
getLineNumber(content, index) {
|
|
393
|
+
return content.substring(0, index).split('\n').length;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
getColumnNumber(content, index) {
|
|
397
|
+
const beforeIndex = content.substring(0, index);
|
|
398
|
+
const lastNewlineIndex = beforeIndex.lastIndexOf('\n');
|
|
399
|
+
return index - lastNewlineIndex;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
module.exports = S052WeakOtpEntropyAnalyzer;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"ruleId": "S052",
|
|
3
|
+
"name": "OTP must have ≥20-bit entropy (≥6 digits) and use CSPRNG",
|
|
4
|
+
"description": "Prevent guessable OTP by enforcing CSPRNG and minimal entropy. Ban non-crypto RNG and too-short codes.",
|
|
5
|
+
"category": "security",
|
|
6
|
+
"severity": "error",
|
|
7
|
+
"options": {
|
|
8
|
+
"otpIdentifiers": [
|
|
9
|
+
"otp","oneTime","one_time","passcode","verificationCode","verifyCode",
|
|
10
|
+
"confirmCode","pin","totp","hotp","resetCode","activationCode"
|
|
11
|
+
],
|
|
12
|
+
"bannedRngApis": {
|
|
13
|
+
"typescript": ["Math\\.random\\s*\\("],
|
|
14
|
+
"javascript": ["Math\\.random\\s*\\("],
|
|
15
|
+
"node": ["crypto\\.pseudoRandomBytes\\s*\\("],
|
|
16
|
+
"java": ["new\\s+Random\\s*\\(","ThreadLocalRandom\\.current\\s*\\("],
|
|
17
|
+
"kotlin": ["kotlin\\.random\\.Random(\\.Default)?"],
|
|
18
|
+
"dart": ["new\\s+Random\\s*\\(","Random\\s*\\("]
|
|
19
|
+
},
|
|
20
|
+
"allowedRngApis": {
|
|
21
|
+
"node": ["crypto\\.randomInt\\s*\\(","crypto\\.randomBytes\\s*\\("],
|
|
22
|
+
"java": ["new\\s+SecureRandom\\s*\\("],
|
|
23
|
+
"kotlin": ["java\\.security\\.SecureRandom"],
|
|
24
|
+
"dart": ["Random\\.secure\\s*\\("]
|
|
25
|
+
},
|
|
26
|
+
"lengthChecks": {
|
|
27
|
+
"numericMinDigits": 6,
|
|
28
|
+
"alphanumericMinLength": 6,
|
|
29
|
+
"regexBadNumeric4": "\\\\b\\\\d{4}\\\\b"
|
|
30
|
+
},
|
|
31
|
+
"totpHotpHints": {
|
|
32
|
+
"requireStandardLib": true,
|
|
33
|
+
"minSecretBits": 128,
|
|
34
|
+
"maxTimeStepSeconds": 30,
|
|
35
|
+
"maxWindow": 1
|
|
36
|
+
},
|
|
37
|
+
"policy": {
|
|
38
|
+
"requireCsprng": true,
|
|
39
|
+
"forbidNonCryptoRng": true,
|
|
40
|
+
"forbidFourDigitOtp": true,
|
|
41
|
+
"requireNoLoggingOfOtp": true,
|
|
42
|
+
"requireSingleUseAndTtl": true
|
|
43
|
+
},
|
|
44
|
+
"detectionHeuristics": {
|
|
45
|
+
"variableNameMatchBoost": true,
|
|
46
|
+
"builderFunctions": ["generateOtp","issueOtp","createCode","sendOtp","totp","hotp"]
|
|
47
|
+
},
|
|
48
|
+
"allowlist": {
|
|
49
|
+
"paths": ["test/","tests/","__tests__/","fixtures/","mocks/"],
|
|
50
|
+
"notes": "Trong test vẫn cấm 4-digit OTP nếu test chạy e2e với hệ thật."
|
|
51
|
+
},
|
|
52
|
+
"thresholds": {
|
|
53
|
+
"maxBannedRngUsages": 0,
|
|
54
|
+
"maxShortOtpPatterns": 0
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Rule S054 - Disallow Default/Built-in Accounts
|
|
2
|
+
|
|
3
|
+
## Mục tiêu
|
|
4
|
+
Ngăn chặn tấn công brute-force và đảm bảo tính truy vết, minh bạch trong kiểm toán. Tránh các tài khoản dễ đoán và dùng chung không rõ danh tính.
|
|
5
|
+
|
|
6
|
+
## Chi tiết
|
|
7
|
+
- Không sử dụng tài khoản mặc định hoặc tài khoản có tên phổ biến (admin, root, sa, test, guest, ...)
|
|
8
|
+
- Mỗi người dùng phải có tài khoản riêng biệt với quyền hạn được giới hạn theo vai trò
|
|
9
|
+
- Bắt buộc đổi mật khẩu mặc định khi khởi tạo hệ thống hoặc khi user được tạo mới
|
|
10
|
+
- Hệ thống cần log mọi lần đăng nhập và truy cập tài nguyên theo từng user cụ thể
|
|
11
|
+
|
|
12
|
+
## Phạm vi kiểm tra
|
|
13
|
+
|
|
14
|
+
### 1. SQL/Seeder/Migration
|
|
15
|
+
- `INSERT INTO users/accounts` with blocked usernames
|
|
16
|
+
- `CREATE USER` statements
|
|
17
|
+
- `GRANT/REVOKE` permissions to default accounts
|
|
18
|
+
|
|
19
|
+
### 2. Code/Application
|
|
20
|
+
- `createUser()`, `new User()`, `addUser()` with blocked usernames
|
|
21
|
+
- User registration/creation functions
|
|
22
|
+
- Authentication/authorization code
|
|
23
|
+
|
|
24
|
+
### 3. Infrastructure as Code (IaC)
|
|
25
|
+
- **Terraform**: `username = "admin"`, `user = "root"`
|
|
26
|
+
- **Helm/Values**: `adminUser:`, `defaultPass:`
|
|
27
|
+
- **Docker/Compose**: `POSTGRES_USER=postgres`, `ENV USER=admin`
|
|
28
|
+
- **Kubernetes**: `serviceAccount: default`, `user: admin`
|
|
29
|
+
|
|
30
|
+
### 4. Configuration Files
|
|
31
|
+
- Database configs: `db.username=admin`
|
|
32
|
+
- Application configs: `auth.user=root`
|
|
33
|
+
- Environment files: `.env` with default credentials
|
|
34
|
+
|
|
35
|
+
### 5. Documentation
|
|
36
|
+
- README/docs with exposed default credentials
|
|
37
|
+
- Login examples with real default accounts
|
|
38
|
+
|
|
39
|
+
## Blocked Usernames
|
|
40
|
+
```
|
|
41
|
+
admin, root, sa, test, guest, operator, super, superuser, sys,
|
|
42
|
+
postgres, mysql, mssql, oracle, elastic, kibana, grafana,
|
|
43
|
+
administrator, demo, example, default, public, anonymous,
|
|
44
|
+
user, password, service, support, backup, monitor
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Password Smells
|
|
48
|
+
```
|
|
49
|
+
password, 123456, admin, Admin@123, Password1, changeme,
|
|
50
|
+
default, qwerty, letmein, welcome, secret, pass123,
|
|
51
|
+
root, toor, administrator, guest
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Exemptions
|
|
55
|
+
- Test directories: `test/`, `tests/`, `__tests__/`, `e2e/`, `spec/`
|
|
56
|
+
- Mock/fixture data
|
|
57
|
+
- Documentation examples (with warnings)
|
|
58
|
+
|
|
59
|
+
## Examples
|
|
60
|
+
|
|
61
|
+
### ❌ Violations
|
|
62
|
+
|
|
63
|
+
**SQL:**
|
|
64
|
+
```sql
|
|
65
|
+
INSERT INTO users (username, password) VALUES ('admin', 'password123');
|
|
66
|
+
CREATE USER 'root'@'localhost' IDENTIFIED BY 'admin';
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**Code:**
|
|
70
|
+
```javascript
|
|
71
|
+
createUser({ username: 'admin', password: 'default' });
|
|
72
|
+
const user = new User('test', 'password');
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Terraform:**
|
|
76
|
+
```hcl
|
|
77
|
+
username = "admin"
|
|
78
|
+
admin_username = "root"
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**Docker:**
|
|
82
|
+
```dockerfile
|
|
83
|
+
ENV POSTGRES_USER=postgres
|
|
84
|
+
ENV MYSQL_USER=root
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**Config:**
|
|
88
|
+
```properties
|
|
89
|
+
spring.datasource.username=admin
|
|
90
|
+
db.username=root
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### ✅ Correct Usage
|
|
94
|
+
|
|
95
|
+
**SQL:**
|
|
96
|
+
```sql
|
|
97
|
+
INSERT INTO users (username, password) VALUES ('john.doe', '${HASHED_PASSWORD}');
|
|
98
|
+
CREATE USER '${DB_USERNAME}'@'localhost' IDENTIFIED BY '${DB_PASSWORD}';
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**Code:**
|
|
102
|
+
```javascript
|
|
103
|
+
createUser({ username: userInput.username, password: hashedPassword });
|
|
104
|
+
const user = new User(registrationForm.username, securePassword);
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Terraform:**
|
|
108
|
+
```hcl
|
|
109
|
+
username = var.database_username
|
|
110
|
+
admin_username = var.admin_user
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**Docker:**
|
|
114
|
+
```dockerfile
|
|
115
|
+
ENV POSTGRES_USER=${DB_USERNAME}
|
|
116
|
+
ENV MYSQL_USER=${DATABASE_USER}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Policy Requirements
|
|
120
|
+
- ✅ `requirePerUserAccount`: true
|
|
121
|
+
- ✅ `requireInitialPasswordChange`: true
|
|
122
|
+
- ✅ `forbidWellKnownServiceAccountsInAppDB`: true
|
|
123
|
+
- ✅ `allowOnlyInEphemeralTests`: true
|
|
124
|
+
- ✅ `mustDisableBuiltInsOnInfra`: true
|
|
125
|
+
|
|
126
|
+
## Thresholds
|
|
127
|
+
- Production: `maxFindings = 0`
|
|
128
|
+
- Test paths: `maxInAllowedPaths = 2`
|
|
129
|
+
- Password smells: `maxPasswordSmells = 0`
|