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