@sun-asterisk/sunlint 1.2.2 → 1.3.1
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 +107 -1
- package/CONTRIBUTING.md +1654 -66
- package/README.md +19 -6
- package/config/ci-cd.json +54 -0
- package/config/development.json +56 -0
- package/config/engines/engines-enhanced.json +86 -0
- package/config/engines/semantic-config.json +114 -0
- package/config/eslint-rule-mapping.json +50 -38
- package/config/large-project.json +143 -0
- package/config/presets/all.json +0 -1
- package/config/release.json +70 -0
- package/config/rule-analysis-strategies.js +23 -4
- package/config/rules/S027-categories.json +122 -0
- package/config/rules/enhanced-rules-registry.json +2564 -0
- package/config/rules/rules-registry-generated.json +785 -837
- package/config/rules/rules-registry.json +13 -1
- package/core/adapters/sunlint-rule-adapter.js +25 -30
- package/core/analysis-orchestrator.js +42 -2
- package/core/categories.js +52 -0
- package/core/category-constants.js +39 -0
- package/core/cli-action-handler.js +53 -32
- package/core/cli-program.js +11 -3
- package/core/config-manager.js +111 -0
- package/core/config-merger.js +88 -0
- package/core/constants/categories.js +168 -0
- package/core/constants/defaults.js +165 -0
- package/core/constants/engines.js +185 -0
- package/core/constants/index.js +30 -0
- package/core/constants/rules.js +215 -0
- package/core/enhanced-rules-registry.js +3 -3
- package/core/file-targeting-service.js +128 -7
- package/core/interfaces/rule-plugin.interface.js +207 -0
- package/core/plugin-manager.js +448 -0
- package/core/rule-selection-service.js +42 -15
- package/core/semantic-engine.js +658 -0
- package/core/semantic-rule-base.js +433 -0
- package/core/unified-rule-registry.js +484 -0
- package/docs/COMMAND-EXAMPLES.md +134 -0
- package/docs/CONSTANTS-ARCHITECTURE.md +288 -0
- package/docs/LARGE-PROJECT-GUIDE.md +324 -0
- package/engines/core/base-engine.js +249 -0
- package/engines/engine-factory.js +275 -0
- package/engines/eslint-engine.js +171 -19
- package/engines/heuristic-engine.js +569 -78
- package/integrations/eslint/plugin/index.js +26 -28
- package/origin-rules/common-en.md +8 -8
- package/package.json +10 -6
- package/rules/common/C003_no_vague_abbreviations/analyzer.js +1 -1
- package/rules/common/C017_constructor_logic/analyzer.js +254 -17
- package/rules/common/C017_constructor_logic/semantic-analyzer.js +340 -0
- package/rules/common/C029_catch_block_logging/analyzer.js +17 -5
- package/rules/common/C033_separate_service_repository/README.md +78 -0
- package/rules/common/C033_separate_service_repository/analyzer.js +160 -0
- package/rules/common/C033_separate_service_repository/config.json +50 -0
- package/rules/common/C033_separate_service_repository/regex-based-analyzer.js +585 -0
- package/rules/common/C033_separate_service_repository/symbol-based-analyzer.js +368 -0
- package/rules/common/C035_error_logging_context/STRATEGY.md +99 -0
- package/rules/common/C035_error_logging_context/analyzer.js +230 -0
- package/rules/common/C035_error_logging_context/config.json +54 -0
- package/rules/common/C035_error_logging_context/regex-based-analyzer.js +299 -0
- package/rules/common/C035_error_logging_context/symbol-based-analyzer.js +454 -0
- package/rules/common/C040_centralized_validation/analyzer.js +165 -0
- package/rules/common/C040_centralized_validation/config.json +46 -0
- package/rules/common/C040_centralized_validation/regex-based-analyzer.js +243 -0
- package/rules/common/C040_centralized_validation/symbol-based-analyzer.js +416 -0
- package/rules/common/C047_no_duplicate_retry_logic/c047-semantic-rule.js +278 -0
- package/rules/common/C047_no_duplicate_retry_logic/symbol-analyzer-enhanced.js +968 -0
- package/rules/common/C047_no_duplicate_retry_logic/symbol-config.json +71 -0
- package/rules/common/{C076_single_test_behavior → C072_single_test_behavior}/analyzer.js +6 -6
- package/rules/common/C076_explicit_function_types/README.md +30 -0
- package/rules/common/C076_explicit_function_types/analyzer.js +172 -0
- package/rules/common/C076_explicit_function_types/config.json +15 -0
- package/rules/common/C076_explicit_function_types/semantic-analyzer.js +341 -0
- package/rules/index.js +8 -0
- package/rules/parser/rule-parser.js +13 -2
- package/rules/security/S005_no_origin_auth/README.md +226 -0
- package/rules/security/S005_no_origin_auth/analyzer.js +184 -0
- package/rules/security/S005_no_origin_auth/ast-analyzer.js +406 -0
- package/rules/security/S005_no_origin_auth/config.json +85 -0
- package/rules/security/S006_no_plaintext_recovery_codes/README.md +139 -0
- package/rules/security/S006_no_plaintext_recovery_codes/analyzer.js +306 -0
- package/rules/security/S006_no_plaintext_recovery_codes/config.json +48 -0
- package/rules/security/S007_no_plaintext_otp/README.md +198 -0
- package/rules/security/S007_no_plaintext_otp/analyzer.js +406 -0
- package/rules/security/S007_no_plaintext_otp/config.json +79 -0
- package/rules/security/S007_no_plaintext_otp/semantic-analyzer.js +609 -0
- package/rules/security/S007_no_plaintext_otp/semantic-config.json +195 -0
- package/rules/security/S007_no_plaintext_otp/semantic-wrapper.js +280 -0
- package/rules/security/S027_no_hardcoded_secrets/analyzer.js +180 -366
- package/rules/security/S027_no_hardcoded_secrets/categories.json +153 -0
- package/rules/security/S027_no_hardcoded_secrets/categorized-analyzer.js +250 -0
- package/scripts/category-manager.js +150 -0
- package/scripts/generate-rules-registry.js +88 -0
- package/scripts/migrate-rule-registry.js +157 -0
- package/scripts/prepare-release.sh +1 -1
- package/scripts/validate-system.js +48 -0
- package/.sunlint.json +0 -35
- package/config/README.md +0 -88
- package/config/engines/eslint-rule-mapping.json +0 -74
- package/config/schemas/sunlint-schema.json +0 -0
- package/config/testing/test-s005-working.ts +0 -22
- package/core/multi-rule-runner.js +0 -0
- package/docs/ESLINT-INTEGRATION-STRATEGY.md +0 -392
- package/docs/FUTURE_PACKAGES.md +0 -83
- package/docs/HEURISTIC_VS_AI.md +0 -113
- package/docs/PRODUCTION_DEPLOYMENT_ANALYSIS.md +0 -112
- package/docs/PRODUCTION_SIZE_IMPACT.md +0 -183
- package/docs/RELEASE_GUIDE.md +0 -230
- package/docs/STANDARDIZED-CATEGORY-FILTERING.md +0 -156
- package/engines/tree-sitter-parser.js +0 -0
- package/engines/universal-ast-engine.js +0 -0
- package/integrations/eslint/plugin/rules/common/c076-single-behavior-per-test.js +0 -254
- package/rules/common/C029_catch_block_logging/analyzer-backup.js +0 -426
- package/rules/common/C029_catch_block_logging/analyzer-fixed.js +0 -130
- package/rules/common/C029_catch_block_logging/analyzer-multi-tech.js +0 -487
- package/rules/common/C029_catch_block_logging/analyzer-simple.js +0 -110
- package/rules/common/C029_catch_block_logging/ast-analyzer-backup.js +0 -441
- package/rules/common/C029_catch_block_logging/ast-analyzer-new.js +0 -127
- package/rules/common/C029_catch_block_logging/ast-analyzer.js +0 -133
- package/rules/common/C029_catch_block_logging/cfg-analyzer.js +0 -408
- package/rules/common/C029_catch_block_logging/dataflow-analyzer.js +0 -454
- package/rules/common/C029_catch_block_logging/multi-language-ast-engine.js +0 -700
- package/rules/common/C029_catch_block_logging/pattern-learning-analyzer.js +0 -568
- package/rules/common/C029_catch_block_logging/semantic-analyzer.js +0 -459
|
@@ -1,436 +1,250 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
-
*/
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
6
3
|
|
|
7
|
-
class
|
|
4
|
+
class S027CategorizedAnalyzer {
|
|
8
5
|
constructor() {
|
|
9
6
|
this.ruleId = 'S027';
|
|
10
|
-
this.ruleName = 'No Hardcoded Secrets';
|
|
11
|
-
this.description = '
|
|
7
|
+
this.ruleName = 'No Hardcoded Secrets (Categorized)';
|
|
8
|
+
this.description = 'Phát hiện thông tin bảo mật theo categories với độ ưu tiên khác nhau';
|
|
12
9
|
|
|
13
|
-
//
|
|
14
|
-
this.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
];
|
|
10
|
+
// Load categories config
|
|
11
|
+
this.config = this.loadConfig();
|
|
12
|
+
this.categories = this.config.categories;
|
|
13
|
+
this.globalExcludePatterns = this.config.global_exclude_patterns.map(p => new RegExp(p, 'i'));
|
|
14
|
+
this.minLength = this.config.min_length || 8;
|
|
15
|
+
this.maxLength = this.config.max_length || 1000;
|
|
57
16
|
|
|
58
|
-
//
|
|
59
|
-
this.
|
|
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
|
-
];
|
|
17
|
+
// Compile patterns for performance
|
|
18
|
+
this.compilePatterns();
|
|
109
19
|
}
|
|
110
|
-
|
|
20
|
+
|
|
21
|
+
loadConfig() {
|
|
22
|
+
const configPath = path.join(__dirname, 'categories.json');
|
|
23
|
+
try {
|
|
24
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
25
|
+
return config.S027;
|
|
26
|
+
} catch (error) {
|
|
27
|
+
console.error('Failed to load S027 categories config:', error.message);
|
|
28
|
+
return { categories: [], global_exclude_patterns: [] };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
compilePatterns() {
|
|
33
|
+
this.categories.forEach(category => {
|
|
34
|
+
category.compiledPatterns = category.patterns.map(p => ({
|
|
35
|
+
regex: new RegExp(p, 'gm'),
|
|
36
|
+
original: p
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
if (category.exclude_patterns) {
|
|
40
|
+
category.compiledExcludePatterns = category.exclude_patterns.map(p => new RegExp(p, 'i'));
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
111
45
|
async analyze(files, language, options = {}) {
|
|
112
46
|
const violations = [];
|
|
47
|
+
this.currentFilePath = '';
|
|
113
48
|
|
|
114
49
|
for (const filePath of files) {
|
|
115
|
-
// Skip build
|
|
116
|
-
if (
|
|
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/')) {
|
|
50
|
+
// Skip build/dist/node_modules
|
|
51
|
+
if (this.shouldSkipFile(filePath)) {
|
|
123
52
|
continue;
|
|
124
53
|
}
|
|
125
54
|
|
|
55
|
+
this.currentFilePath = filePath;
|
|
56
|
+
|
|
126
57
|
try {
|
|
127
|
-
const content =
|
|
58
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
128
59
|
const fileViolations = this.analyzeFile(content, filePath);
|
|
129
60
|
violations.push(...fileViolations);
|
|
130
61
|
} catch (error) {
|
|
131
|
-
|
|
62
|
+
if (options.verbose) {
|
|
63
|
+
console.error(`Error analyzing ${filePath}:`, error.message);
|
|
64
|
+
}
|
|
132
65
|
}
|
|
133
66
|
}
|
|
134
67
|
|
|
135
68
|
return violations;
|
|
136
69
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
});
|
|
70
|
+
|
|
71
|
+
shouldSkipFile(filePath) {
|
|
72
|
+
const skipPatterns = [
|
|
73
|
+
'build/', 'dist/', 'node_modules/', '.git/',
|
|
74
|
+
'coverage/', '.next/', '.cache/', 'tmp/',
|
|
75
|
+
'.lock', '.log', '.min.js', '.bundle.js'
|
|
76
|
+
];
|
|
177
77
|
|
|
178
|
-
return
|
|
78
|
+
return skipPatterns.some(pattern => filePath.includes(pattern));
|
|
179
79
|
}
|
|
180
80
|
|
|
181
|
-
|
|
182
|
-
const
|
|
183
|
-
|
|
81
|
+
analyzeFile(content, filePath) {
|
|
82
|
+
const violations = [];
|
|
83
|
+
// Handle different line endings (Windows \r\n, Unix \n, Mac \r)
|
|
84
|
+
const lines = content.split(/\r?\n/);
|
|
85
|
+
|
|
86
|
+
// Check if this is a test file for context
|
|
87
|
+
const isTestFile = this.isTestFile(filePath);
|
|
184
88
|
|
|
185
89
|
lines.forEach((line, index) => {
|
|
90
|
+
const lineNumber = index + 1;
|
|
186
91
|
const trimmedLine = line.trim();
|
|
187
|
-
const lineKey = `${index}:${trimmedLine}`;
|
|
188
92
|
|
|
189
|
-
// Skip comments
|
|
190
|
-
if (this.isCommentOrImport(trimmedLine)
|
|
93
|
+
// Skip comments and imports
|
|
94
|
+
if (this.isCommentOrImport(trimmedLine)) {
|
|
191
95
|
return;
|
|
192
96
|
}
|
|
193
97
|
|
|
194
|
-
//
|
|
195
|
-
|
|
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)) {
|
|
98
|
+
// Check global exclude patterns first
|
|
99
|
+
if (this.matchesGlobalExcludes(line)) {
|
|
244
100
|
return;
|
|
245
101
|
}
|
|
246
102
|
|
|
247
|
-
//
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
}
|
|
103
|
+
// Check each category
|
|
104
|
+
this.categories.forEach(category => {
|
|
105
|
+
const categoryViolations = this.checkCategory(
|
|
106
|
+
category, line, lineNumber, filePath, isTestFile
|
|
107
|
+
);
|
|
108
|
+
violations.push(...categoryViolations);
|
|
262
109
|
});
|
|
263
110
|
});
|
|
264
111
|
|
|
265
|
-
return
|
|
112
|
+
return violations;
|
|
266
113
|
}
|
|
267
114
|
|
|
268
|
-
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
});
|
|
280
|
-
}
|
|
281
|
-
}
|
|
115
|
+
isTestFile(filePath) {
|
|
116
|
+
const testPatterns = [
|
|
117
|
+
/\.(test|spec)\./i,
|
|
118
|
+
/__tests__/i,
|
|
119
|
+
/\/tests?\//i,
|
|
120
|
+
/\/spec\//i,
|
|
121
|
+
/setupTests/i,
|
|
122
|
+
/testSetup/i,
|
|
123
|
+
/test[-_]/i, // Matches test- or test_
|
|
124
|
+
/^.*\/test[^\/]*\.js$/i // Matches files starting with test
|
|
125
|
+
];
|
|
282
126
|
|
|
283
|
-
return
|
|
127
|
+
return testPatterns.some(pattern => pattern.test(filePath));
|
|
284
128
|
}
|
|
285
129
|
|
|
286
|
-
|
|
287
|
-
//
|
|
288
|
-
|
|
289
|
-
|
|
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;
|
|
130
|
+
isCommentOrImport(line) {
|
|
131
|
+
return line.startsWith('//') || line.startsWith('/*') ||
|
|
132
|
+
line.startsWith('import') || line.startsWith('export') ||
|
|
133
|
+
line.startsWith('*') || line.startsWith('<');
|
|
308
134
|
}
|
|
309
135
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
return testIndicators.some(indicator => line.includes(indicator));
|
|
136
|
+
matchesGlobalExcludes(line) {
|
|
137
|
+
return this.globalExcludePatterns.some(pattern => pattern.test(line));
|
|
313
138
|
}
|
|
314
139
|
|
|
315
|
-
|
|
316
|
-
const
|
|
140
|
+
checkCategory(category, line, lineNumber, filePath, isTestFile) {
|
|
141
|
+
const violations = [];
|
|
317
142
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
// Skip comments, imports, and test files
|
|
322
|
-
if (this.isCommentOrImport(trimmedLine) || this.isTestFile(trimmedLine)) {
|
|
323
|
-
return;
|
|
324
|
-
}
|
|
143
|
+
category.compiledPatterns.forEach(({ regex, original }) => {
|
|
144
|
+
let match;
|
|
325
145
|
|
|
326
|
-
//
|
|
327
|
-
|
|
146
|
+
// Reset regex lastIndex for global patterns
|
|
147
|
+
regex.lastIndex = 0;
|
|
328
148
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
pattern: secretPattern,
|
|
337
|
-
value: literal.value,
|
|
338
|
-
originalLine: line
|
|
339
|
-
});
|
|
149
|
+
while ((match = regex.exec(line)) !== null) {
|
|
150
|
+
const matchedText = match[0];
|
|
151
|
+
const column = match.index + 1;
|
|
152
|
+
|
|
153
|
+
// Check length constraints
|
|
154
|
+
if (matchedText.length < this.minLength || matchedText.length > this.maxLength) {
|
|
155
|
+
continue;
|
|
340
156
|
}
|
|
341
|
-
|
|
157
|
+
|
|
158
|
+
// Check category-specific excludes
|
|
159
|
+
if (category.compiledExcludePatterns &&
|
|
160
|
+
category.compiledExcludePatterns.some(pattern => pattern.test(matchedText))) {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Be more lenient in test files for lower severity categories
|
|
165
|
+
// But still report critical/high severity issues even in test files
|
|
166
|
+
if (isTestFile && category.severity === 'low') {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
violations.push({
|
|
171
|
+
file: filePath,
|
|
172
|
+
line: lineNumber,
|
|
173
|
+
column: column,
|
|
174
|
+
message: `[${category.name}] Potential ${category.severity} security risk: '${matchedText}'. ${category.description}`,
|
|
175
|
+
severity: this.mapSeverity(category.severity),
|
|
176
|
+
ruleId: this.ruleId,
|
|
177
|
+
category: category.name,
|
|
178
|
+
categoryDescription: category.description,
|
|
179
|
+
matchedPattern: original,
|
|
180
|
+
matchedText: matchedText
|
|
181
|
+
});
|
|
182
|
+
}
|
|
342
183
|
});
|
|
343
184
|
|
|
344
|
-
return
|
|
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('<');
|
|
185
|
+
return violations;
|
|
356
186
|
}
|
|
357
187
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
188
|
+
mapSeverity(categorySeverity) {
|
|
189
|
+
const severityMap = {
|
|
190
|
+
'critical': 'error',
|
|
191
|
+
'high': 'warning',
|
|
192
|
+
'medium': 'warning',
|
|
193
|
+
'low': 'info'
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
return severityMap[categorySeverity] || 'warning';
|
|
365
197
|
}
|
|
366
198
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
199
|
+
// Method for getting category statistics
|
|
200
|
+
getCategoryStats(violations) {
|
|
201
|
+
const stats = {};
|
|
202
|
+
|
|
203
|
+
violations.forEach(violation => {
|
|
204
|
+
const category = violation.category;
|
|
205
|
+
if (!stats[category]) {
|
|
206
|
+
stats[category] = {
|
|
207
|
+
count: 0,
|
|
208
|
+
severity: violation.severity,
|
|
209
|
+
files: new Set()
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
stats[category].count++;
|
|
213
|
+
stats[category].files.add(violation.file);
|
|
214
|
+
});
|
|
379
215
|
|
|
380
|
-
//
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
216
|
+
// Convert Set to array for JSON serialization
|
|
217
|
+
Object.keys(stats).forEach(category => {
|
|
218
|
+
stats[category].files = Array.from(stats[category].files);
|
|
219
|
+
stats[category].fileCount = stats[category].files.length;
|
|
220
|
+
});
|
|
384
221
|
|
|
385
|
-
|
|
386
|
-
return this.looksLikeSecret(value);
|
|
222
|
+
return stats;
|
|
387
223
|
}
|
|
388
224
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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;
|
|
225
|
+
// Method for filtering by category
|
|
226
|
+
filterByCategory(violations, categoryNames) {
|
|
227
|
+
if (!categoryNames || categoryNames.length === 0) {
|
|
228
|
+
return violations;
|
|
410
229
|
}
|
|
411
230
|
|
|
412
|
-
return
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
isDynamicValue(valueExpr) {
|
|
416
|
-
return this.dynamicPatterns.some(pattern => pattern.test(valueExpr));
|
|
231
|
+
return violations.filter(violation =>
|
|
232
|
+
categoryNames.includes(violation.category)
|
|
233
|
+
);
|
|
417
234
|
}
|
|
418
235
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
}
|
|
236
|
+
// Method for filtering by severity
|
|
237
|
+
filterBySeverity(violations, minSeverity = 'info') {
|
|
238
|
+
const severityOrder = ['info', 'warning', 'error'];
|
|
239
|
+
const minIndex = severityOrder.indexOf(minSeverity);
|
|
424
240
|
|
|
425
|
-
|
|
426
|
-
const commonValues = ['password', 'bearer', 'basic', 'token', 'key', 'secret', 'admin', 'user'];
|
|
427
|
-
if (commonValues.includes(value.toLowerCase())) {
|
|
428
|
-
return false;
|
|
429
|
-
}
|
|
241
|
+
if (minIndex === -1) return violations;
|
|
430
242
|
|
|
431
|
-
|
|
432
|
-
|
|
243
|
+
return violations.filter(violation => {
|
|
244
|
+
const violationIndex = severityOrder.indexOf(violation.severity);
|
|
245
|
+
return violationIndex >= minIndex;
|
|
246
|
+
});
|
|
433
247
|
}
|
|
434
248
|
}
|
|
435
249
|
|
|
436
|
-
module.exports =
|
|
250
|
+
module.exports = S027CategorizedAnalyzer;
|