@sun-asterisk/sunlint 1.3.0 → 1.3.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/CHANGELOG.md +115 -1
- package/CONTRIBUTING.md +249 -605
- package/README.md +3 -4
- package/config/ci-cd.json +54 -0
- package/config/development.json +56 -0
- 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 +38 -3
- package/config/rules/enhanced-rules-registry.json +474 -1179
- package/config/rules/rules-registry-generated.json +3 -3
- package/core/cli-action-handler.js +24 -30
- package/core/cli-program.js +11 -3
- package/core/config-merger.js +29 -2
- package/core/enhanced-rules-registry.js +3 -2
- package/core/semantic-engine.js +129 -19
- package/core/semantic-rule-base.js +4 -2
- package/core/unified-rule-registry.js +1 -1
- package/docs/COMMAND-EXAMPLES.md +134 -0
- package/docs/LARGE-PROJECT-GUIDE.md +324 -0
- package/engines/heuristic-engine.js +135 -16
- package/integrations/eslint/plugin/index.js +0 -2
- package/integrations/eslint/plugin/rules/common/c003-no-vague-abbreviations.js +59 -1
- package/integrations/eslint/plugin/rules/common/c006-function-name-verb-noun.js +26 -1
- package/integrations/eslint/plugin/rules/common/c030-use-custom-error-classes.js +54 -19
- package/origin-rules/common-en.md +19 -15
- package/package.json +1 -1
- package/rules/common/C002_no_duplicate_code/analyzer.js +334 -36
- package/rules/common/C003_no_vague_abbreviations/analyzer.js +220 -35
- package/rules/common/C006_function_naming/analyzer.js +29 -3
- package/rules/common/C010_limit_block_nesting/analyzer.js +181 -337
- package/rules/common/C010_limit_block_nesting/config.json +64 -0
- package/rules/common/C010_limit_block_nesting/regex-based-analyzer.js +379 -0
- package/rules/common/C010_limit_block_nesting/symbol-based-analyzer.js +231 -0
- package/rules/common/C013_no_dead_code/analyzer.js +75 -177
- package/rules/common/C013_no_dead_code/config.json +61 -0
- package/rules/common/C013_no_dead_code/regex-based-analyzer.js +345 -0
- package/rules/common/C013_no_dead_code/symbol-based-analyzer.js +640 -0
- package/rules/common/C014_dependency_injection/analyzer.js +48 -313
- package/rules/common/C014_dependency_injection/config.json +26 -0
- package/rules/common/C014_dependency_injection/symbol-based-analyzer.js +751 -0
- package/rules/common/C017_constructor_logic/analyzer.js +254 -17
- package/rules/common/C017_constructor_logic/semantic-analyzer.js +340 -0
- package/rules/common/C018_no_throw_generic_error/analyzer.js +232 -0
- package/rules/common/C018_no_throw_generic_error/config.json +50 -0
- package/rules/common/C018_no_throw_generic_error/regex-based-analyzer.js +387 -0
- package/rules/common/C018_no_throw_generic_error/symbol-based-analyzer.js +314 -0
- package/rules/common/C019_log_level_usage/analyzer.js +110 -317
- package/rules/common/C019_log_level_usage/pattern-analyzer.js +88 -0
- package/rules/common/C019_log_level_usage/system-log-analyzer.js +1267 -0
- package/rules/common/C023_no_duplicate_variable/analyzer.js +180 -0
- package/rules/common/C023_no_duplicate_variable/config.json +50 -0
- package/rules/common/C023_no_duplicate_variable/symbol-based-analyzer.js +158 -0
- package/rules/common/C024_no_scatter_hardcoded_constants/analyzer.js +180 -0
- package/rules/common/C024_no_scatter_hardcoded_constants/config.json +50 -0
- package/rules/common/C024_no_scatter_hardcoded_constants/symbol-based-analyzer.js +181 -0
- package/rules/common/C030_use_custom_error_classes/analyzer.js +200 -0
- 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 +232 -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/{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 +6 -1
- 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/S009_no_insecure_encryption/README.md +158 -0
- package/rules/security/S009_no_insecure_encryption/analyzer.js +319 -0
- package/rules/security/S009_no_insecure_encryption/config.json +55 -0
- package/rules/security/S010_no_insecure_encryption/README.md +224 -0
- package/rules/security/S010_no_insecure_encryption/analyzer.js +493 -0
- package/rules/security/S010_no_insecure_encryption/config.json +48 -0
- package/rules/security/S016_no_sensitive_querystring/STRATEGY.md +149 -0
- package/rules/security/S016_no_sensitive_querystring/analyzer.js +276 -0
- package/rules/security/S016_no_sensitive_querystring/config.json +127 -0
- package/rules/security/S016_no_sensitive_querystring/regex-based-analyzer.js +258 -0
- package/rules/security/S016_no_sensitive_querystring/symbol-based-analyzer.js +495 -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/rules/security/S048_no_current_password_in_reset/README.md +222 -0
- package/rules/security/S048_no_current_password_in_reset/analyzer.js +366 -0
- package/rules/security/S048_no_current_password_in_reset/config.json +48 -0
- package/rules/security/S055_content_type_validation/README.md +176 -0
- package/rules/security/S055_content_type_validation/analyzer.js +312 -0
- package/rules/security/S055_content_type_validation/config.json +48 -0
- package/rules/utils/rule-helpers.js +140 -1
- package/scripts/consolidate-config.js +116 -0
- package/scripts/prepare-release.sh +1 -1
- package/config/rules/rules-registry.json +0 -765
- 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/integrations/eslint/plugin/rules/common/c076-single-behavior-per-test.js +0 -254
- package/rules/common/C006_function_naming/smart-analyzer.js +0 -503
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
{
|
|
2
|
+
"S027": {
|
|
3
|
+
"categories": [
|
|
4
|
+
{
|
|
5
|
+
"name": "AWS Credentials",
|
|
6
|
+
"severity": "critical",
|
|
7
|
+
"description": "AWS access keys, secret keys, and session tokens",
|
|
8
|
+
"patterns": [
|
|
9
|
+
"AKIA[0-9A-Z]{16}",
|
|
10
|
+
"(aws[-_]?)?(secret[-_]?access[-_]?key|access[-_]?key[-_]?id|awssecretaccesskey|awsaccesskeyid)\\s*[=:]\\s*[\"']?[A-Za-z0-9\\/+=]{20,40}[\"']?",
|
|
11
|
+
"(aws[-_]?)?session[-_]?token\\s*[=:]\\s*[\"']?[A-Za-z0-9\\/+=]{100,}[\"']?"
|
|
12
|
+
],
|
|
13
|
+
"exclude_patterns": [
|
|
14
|
+
"(test|mock|fake|example|demo)[-_]?aws",
|
|
15
|
+
"AWS_REGION|AWS_DEFAULT_REGION"
|
|
16
|
+
]
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"name": "JWT & Authentication Tokens",
|
|
20
|
+
"severity": "critical",
|
|
21
|
+
"description": "JWT tokens and authentication credentials",
|
|
22
|
+
"patterns": [
|
|
23
|
+
"eyJ[A-Za-z0-9\\-_=]+\\.[A-Za-z0-9\\-_=]+\\.?[A-Za-z0-9\\-_.+/=]*",
|
|
24
|
+
"(jwt|bearer|auth|authtoken|jwttoken)[-_]?(token|secret)?\\s*[=:]\\s*[\"']?[a-zA-Z0-9\\-_=]{20,}[\"']?",
|
|
25
|
+
"authorization\\s*[=:]\\s*[\"']?(bearer|basic)\\s+[a-zA-Z0-9\\-_=]{10,}[\"']?"
|
|
26
|
+
]
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"name": "API Keys & Secrets",
|
|
30
|
+
"severity": "high",
|
|
31
|
+
"description": "Generic API keys and secret tokens",
|
|
32
|
+
"patterns": [
|
|
33
|
+
"(api[-_]?key|apikey|secret[-_]?key|secretkey|access[-_]?token|accesstoken)\\s*[=:]\\s*[\"']?[a-zA-Z0-9\\-_=]{16,}[\"']?",
|
|
34
|
+
"(client[-_]?secret|clientsecret|app[-_]?secret|appsecret)\\s*[=:]\\s*[\"']?[a-zA-Z0-9\\-_=]{20,}[\"']?",
|
|
35
|
+
"(private[-_]?key|privatekey|encryption[-_]?key|encryptionkey)\\s*[=:]\\s*[\"']?[a-zA-Z0-9\\-_=]{20,}[\"']?",
|
|
36
|
+
"(password|secret)\\s*[=:]\\s*[\"'][a-zA-Z0-9\\-_=]{6,}[\"']"
|
|
37
|
+
],
|
|
38
|
+
"exclude_patterns": [
|
|
39
|
+
"(display|row|sort|primary|foreign)[-_]?key",
|
|
40
|
+
"key(value|path|name|code|id|index)",
|
|
41
|
+
"^key$",
|
|
42
|
+
"(test|mock|demo|example).*password",
|
|
43
|
+
"password.*(test|mock|demo|example|123|dummy)",
|
|
44
|
+
"wrongpassword|correctpassword|testpassword"
|
|
45
|
+
]
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"name": "Database Credentials",
|
|
49
|
+
"severity": "high",
|
|
50
|
+
"description": "Database connection strings and passwords",
|
|
51
|
+
"patterns": [
|
|
52
|
+
"(mongodb|mysql|postgres|redis):\\/\\/[^\\/\\s'\"]+:[^\\/\\s'\"]+@[^\\/\\s'\"]+",
|
|
53
|
+
"(db|database|dbpassword|databasepassword)[-_]?(password|pass|pwd|secret)?\\s*[=:]\\s*[\"']?[a-zA-Z0-9\\-_=]{6,}[\"']?",
|
|
54
|
+
"connection[-_]?string\\s*[=:]\\s*[\"']?[^\"'\\s]{20,}[\"']?"
|
|
55
|
+
]
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"name": "Third-party Service Keys",
|
|
59
|
+
"severity": "high",
|
|
60
|
+
"description": "GitHub, Slack, Stripe and other service tokens",
|
|
61
|
+
"patterns": [
|
|
62
|
+
"gh[pousr]_[A-Za-z0-9_]{36}",
|
|
63
|
+
"xox[baprs]-[A-Za-z0-9-]+",
|
|
64
|
+
"sk_live_[A-Za-z0-9]{24,}",
|
|
65
|
+
"(github|slack|stripe|paypal)[-_]?(token|key|secret)[\\s:=]+[\"']?[a-zA-Z0-9\\-_=]{16,}[\"']?"
|
|
66
|
+
]
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"name": "Suspicious Variable Names",
|
|
70
|
+
"severity": "medium",
|
|
71
|
+
"description": "Variables with sensitive naming patterns",
|
|
72
|
+
"patterns": [
|
|
73
|
+
"(client|app|service)[-_]?(id|key|token|secret)[\"']?\\s*[:=]\\s*[\"'][A-Za-z0-9\\-_=]{12,}[\"']?",
|
|
74
|
+
"(oauth|openid)[-_]?(client[-_]?id|secret)[\\s:=]+[\"']?[a-zA-Z0-9\\-_=]{10,}[\"']?"
|
|
75
|
+
],
|
|
76
|
+
"exclude_patterns": [
|
|
77
|
+
"(send|verify|update|register|reset).*password",
|
|
78
|
+
"password.*(reset|verify|update|first|time)"
|
|
79
|
+
]
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"name": "Base64 Encoded Secrets",
|
|
83
|
+
"severity": "medium",
|
|
84
|
+
"description": "Potentially encoded sensitive data",
|
|
85
|
+
"patterns": [
|
|
86
|
+
"(?:^|[\\s=:'\"])([A-Za-z0-9+\\/]{64,}={0,2})(?:[\\s'\";}]|$)"
|
|
87
|
+
],
|
|
88
|
+
"exclude_patterns": [
|
|
89
|
+
"^[a-zA-Z0-9+\\/]*$",
|
|
90
|
+
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
|
|
91
|
+
"(test|demo|example|sample|mock)",
|
|
92
|
+
"^\\/[a-zA-Z0-9\\/\\-_]+$",
|
|
93
|
+
"^[a-zA-Z0-9\\/\\-_\\.]+$",
|
|
94
|
+
"[a-zA-Z]+[a-zA-Z\\/\\-_]{20,}",
|
|
95
|
+
"[a-zA-Z]+Slice\\/",
|
|
96
|
+
"[a-zA-Z]+Company\\/",
|
|
97
|
+
"[a-zA-Z]+Management\\/",
|
|
98
|
+
"[a-zA-Z]+Component",
|
|
99
|
+
"[a-zA-Z]+Setting",
|
|
100
|
+
"[a-zA-Z]+Match",
|
|
101
|
+
"Selector$",
|
|
102
|
+
"Controller$",
|
|
103
|
+
"Service$",
|
|
104
|
+
"Api$",
|
|
105
|
+
"slice\\/",
|
|
106
|
+
"Component\\/",
|
|
107
|
+
"management\\/",
|
|
108
|
+
"company\\/",
|
|
109
|
+
"import.*from",
|
|
110
|
+
"require\\("
|
|
111
|
+
]
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"name": "Environment Variables",
|
|
115
|
+
"severity": "low",
|
|
116
|
+
"description": "Public environment variables that might leak info",
|
|
117
|
+
"patterns": [
|
|
118
|
+
"NEXT_PUBLIC_[A-Z0-9_]+[\\s:=]+[\"'][^\"']+[\"']",
|
|
119
|
+
"react_app_[A-Z0-9_]+[\\s:=]+[\"'][^\"']+[\"']"
|
|
120
|
+
],
|
|
121
|
+
"exclude_patterns": [
|
|
122
|
+
"NODE_ENV|ENV|ENVIRONMENT|MODE|DEBUG"
|
|
123
|
+
]
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"name": "File Path Leaks",
|
|
127
|
+
"severity": "low",
|
|
128
|
+
"description": "Sensitive file patterns",
|
|
129
|
+
"patterns": [
|
|
130
|
+
"^\\s*['\"]?\\.env(\\.[a-zA-Z0-9_]+)?['\"]?\\s*$",
|
|
131
|
+
"^\\s*['\"]?(secrets?|credentials?|private[-_]?keys?)\\.(json|ya?ml|ts|js)['\"]?\\s*$",
|
|
132
|
+
"^\\s*['\"]?(id_rsa|id_dsa|\\.pem|\\.p12|\\.pfx)['\"]?\\s*$"
|
|
133
|
+
],
|
|
134
|
+
"exclude_patterns": [
|
|
135
|
+
"process\\.env\\.",
|
|
136
|
+
"import.*from",
|
|
137
|
+
"require\\(",
|
|
138
|
+
"NODE_ENV|ENVIRONMENT|MODE|DEBUG"
|
|
139
|
+
]
|
|
140
|
+
}
|
|
141
|
+
],
|
|
142
|
+
"global_exclude_patterns": [
|
|
143
|
+
"(test|mock|fake|dummy|placeholder)\\.(js|ts|jsx|tsx)$",
|
|
144
|
+
"\\.(test|spec|mock)\\.",
|
|
145
|
+
"__tests__|\\/tests?\\/|\\/spec\\/",
|
|
146
|
+
"process\\.env\\.",
|
|
147
|
+
"import.*from.*['\"]",
|
|
148
|
+
"require\\(['\"]"
|
|
149
|
+
],
|
|
150
|
+
"min_length": 8,
|
|
151
|
+
"max_length": 1000
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
class S027CategorizedAnalyzer {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.ruleId = 'S027';
|
|
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';
|
|
9
|
+
|
|
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;
|
|
16
|
+
|
|
17
|
+
// Compile patterns for performance
|
|
18
|
+
this.compilePatterns();
|
|
19
|
+
}
|
|
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
|
+
|
|
45
|
+
async analyze(files, language, options = {}) {
|
|
46
|
+
const violations = [];
|
|
47
|
+
this.currentFilePath = '';
|
|
48
|
+
|
|
49
|
+
for (const filePath of files) {
|
|
50
|
+
// Skip build/dist/node_modules
|
|
51
|
+
if (this.shouldSkipFile(filePath)) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
this.currentFilePath = filePath;
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
59
|
+
const fileViolations = this.analyzeFile(content, filePath);
|
|
60
|
+
violations.push(...fileViolations);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
if (options.verbose) {
|
|
63
|
+
console.error(`Error analyzing ${filePath}:`, error.message);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return violations;
|
|
69
|
+
}
|
|
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
|
+
];
|
|
77
|
+
|
|
78
|
+
return skipPatterns.some(pattern => filePath.includes(pattern));
|
|
79
|
+
}
|
|
80
|
+
|
|
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);
|
|
88
|
+
|
|
89
|
+
lines.forEach((line, index) => {
|
|
90
|
+
const lineNumber = index + 1;
|
|
91
|
+
const trimmedLine = line.trim();
|
|
92
|
+
|
|
93
|
+
// Skip comments and imports
|
|
94
|
+
if (this.isCommentOrImport(trimmedLine)) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Check global exclude patterns first
|
|
99
|
+
if (this.matchesGlobalExcludes(line)) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
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);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return violations;
|
|
113
|
+
}
|
|
114
|
+
|
|
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
|
+
];
|
|
126
|
+
|
|
127
|
+
return testPatterns.some(pattern => pattern.test(filePath));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
isCommentOrImport(line) {
|
|
131
|
+
return line.startsWith('//') || line.startsWith('/*') ||
|
|
132
|
+
line.startsWith('import') || line.startsWith('export') ||
|
|
133
|
+
line.startsWith('*') || line.startsWith('<');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
matchesGlobalExcludes(line) {
|
|
137
|
+
return this.globalExcludePatterns.some(pattern => pattern.test(line));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
checkCategory(category, line, lineNumber, filePath, isTestFile) {
|
|
141
|
+
const violations = [];
|
|
142
|
+
|
|
143
|
+
category.compiledPatterns.forEach(({ regex, original }) => {
|
|
144
|
+
let match;
|
|
145
|
+
|
|
146
|
+
// Reset regex lastIndex for global patterns
|
|
147
|
+
regex.lastIndex = 0;
|
|
148
|
+
|
|
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;
|
|
156
|
+
}
|
|
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
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
return violations;
|
|
186
|
+
}
|
|
187
|
+
|
|
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';
|
|
197
|
+
}
|
|
198
|
+
|
|
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
|
+
});
|
|
215
|
+
|
|
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
|
+
});
|
|
221
|
+
|
|
222
|
+
return stats;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Method for filtering by category
|
|
226
|
+
filterByCategory(violations, categoryNames) {
|
|
227
|
+
if (!categoryNames || categoryNames.length === 0) {
|
|
228
|
+
return violations;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return violations.filter(violation =>
|
|
232
|
+
categoryNames.includes(violation.category)
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Method for filtering by severity
|
|
237
|
+
filterBySeverity(violations, minSeverity = 'info') {
|
|
238
|
+
const severityOrder = ['info', 'warning', 'error'];
|
|
239
|
+
const minIndex = severityOrder.indexOf(minSeverity);
|
|
240
|
+
|
|
241
|
+
if (minIndex === -1) return violations;
|
|
242
|
+
|
|
243
|
+
return violations.filter(violation => {
|
|
244
|
+
const violationIndex = severityOrder.indexOf(violation.severity);
|
|
245
|
+
return violationIndex >= minIndex;
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
module.exports = S027CategorizedAnalyzer;
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# S048 - No Current Password in Reset Process
|
|
2
|
+
|
|
3
|
+
## Mô tả
|
|
4
|
+
|
|
5
|
+
Rule này kiểm tra xem các quy trình đặt lại mật khẩu có yêu cầu mật khẩu hiện tại hay không. Việc yêu cầu mật khẩu hiện tại trong quy trình reset mật khẩu vi phạm nguyên tắc bảo mật và làm mất đi mục đích của tính năng "quên mật khẩu".
|
|
6
|
+
|
|
7
|
+
## Mục tiêu
|
|
8
|
+
|
|
9
|
+
- Ngăn chặn việc yêu cầu mật khẩu hiện tại trong quy trình reset mật khẩu
|
|
10
|
+
- Đảm bảo quy trình reset mật khẩu được thiết kế an toàn và hợp lý
|
|
11
|
+
- Tuân thủ OWASP A04:2021 - Insecure Design và CWE-640
|
|
12
|
+
|
|
13
|
+
## Chi tiết Rule
|
|
14
|
+
|
|
15
|
+
### Phát hiện lỗi khi:
|
|
16
|
+
|
|
17
|
+
1. **API endpoints yêu cầu current password trong reset**:
|
|
18
|
+
```javascript
|
|
19
|
+
app.post('/reset-password', (req, res) => {
|
|
20
|
+
const { currentPassword, newPassword } = req.body; // ❌ Yêu cầu mật khẩu hiện tại
|
|
21
|
+
if (!validateCurrentPassword(currentPassword)) {
|
|
22
|
+
return res.status(400).json({ error: 'Current password incorrect' });
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
2. **Form validation yêu cầu current password**:
|
|
28
|
+
```typescript
|
|
29
|
+
const resetPasswordSchema = {
|
|
30
|
+
currentPassword: { type: String, required: true }, // ❌ Bắt buộc mật khẩu hiện tại
|
|
31
|
+
newPassword: { type: String, required: true }
|
|
32
|
+
};
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
3. **Service methods kiểm tra current password trong reset**:
|
|
36
|
+
```javascript
|
|
37
|
+
async resetPassword(userId, currentPassword, newPassword) {
|
|
38
|
+
const user = await User.findById(userId);
|
|
39
|
+
if (!user.validatePassword(currentPassword)) { // ❌ Validate mật khẩu hiện tại
|
|
40
|
+
throw new Error('Current password is incorrect');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
4. **React components với current password field**:
|
|
46
|
+
```typescript
|
|
47
|
+
function ResetPasswordForm() {
|
|
48
|
+
return (
|
|
49
|
+
<form>
|
|
50
|
+
<input name="currentPassword" required /> {/* ❌ Trường mật khẩu hiện tại */}
|
|
51
|
+
<input name="newPassword" required />
|
|
52
|
+
</form>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Cách khắc phục:
|
|
58
|
+
|
|
59
|
+
1. **Sử dụng token-based reset**:
|
|
60
|
+
```javascript
|
|
61
|
+
app.post('/reset-password', (req, res) => {
|
|
62
|
+
const { token, newPassword } = req.body; // ✅ Sử dụng token thay vì current password
|
|
63
|
+
if (!validateResetToken(token)) {
|
|
64
|
+
return res.status(400).json({ error: 'Invalid reset token' });
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
2. **Schema với reset token**:
|
|
70
|
+
```typescript
|
|
71
|
+
const resetPasswordSchema = {
|
|
72
|
+
resetToken: { type: String, required: true }, // ✅ Token reset an toàn
|
|
73
|
+
newPassword: { type: String, required: true }
|
|
74
|
+
};
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
3. **Service method an toàn**:
|
|
78
|
+
```javascript
|
|
79
|
+
async resetPasswordWithToken(resetToken, newPassword) {
|
|
80
|
+
const tokenData = await validateResetToken(resetToken); // ✅ Validate token
|
|
81
|
+
if (!tokenData.valid) {
|
|
82
|
+
throw new Error('Invalid or expired reset token');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
4. **Form với email verification**:
|
|
88
|
+
```typescript
|
|
89
|
+
function ForgotPasswordForm() {
|
|
90
|
+
return (
|
|
91
|
+
<form>
|
|
92
|
+
<input name="email" type="email" required /> {/* ✅ Chỉ cần email */}
|
|
93
|
+
<button>Send Reset Link</button>
|
|
94
|
+
</form>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Tại sao đây là vấn đề bảo mật?
|
|
100
|
+
|
|
101
|
+
### 1. **Mâu thuẫn logic**
|
|
102
|
+
- Nếu người dùng nhớ mật khẩu hiện tại, họ không cần reset
|
|
103
|
+
- Yêu cầu mật khẩu hiện tại làm vô hiệu hóa tính năng "quên mật khẩu"
|
|
104
|
+
|
|
105
|
+
### 2. **Tạo điểm yếu bảo mật**
|
|
106
|
+
- Kẻ tấn công có thể lợi dụng để brute force mật khẩu
|
|
107
|
+
- Tăng surface attack cho account takeover
|
|
108
|
+
|
|
109
|
+
### 3. **Trải nghiệm người dùng kém**
|
|
110
|
+
- Người dùng quên mật khẩu không thể hoàn thành quy trình reset
|
|
111
|
+
- Dẫn đến khóa tài khoản và frustration
|
|
112
|
+
|
|
113
|
+
## Các trường hợp ngoại lệ
|
|
114
|
+
|
|
115
|
+
### Trường hợp hợp lệ (không phải lỗi):
|
|
116
|
+
|
|
117
|
+
1. **Password Change (không phải Reset)**:
|
|
118
|
+
```javascript
|
|
119
|
+
// ✅ Thay đổi mật khẩu khi đã đăng nhập - hợp lệ
|
|
120
|
+
app.post('/change-password', authenticateUser, (req, res) => {
|
|
121
|
+
const { currentPassword, newPassword } = req.body;
|
|
122
|
+
// Hợp lệ vì đây là thay đổi, không phải reset
|
|
123
|
+
});
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
2. **Profile settings**:
|
|
127
|
+
```javascript
|
|
128
|
+
// ✅ Cập nhật mật khẩu trong settings - hợp lệ
|
|
129
|
+
function ProfileSettings() {
|
|
130
|
+
return (
|
|
131
|
+
<div>
|
|
132
|
+
<h2>Change Password</h2> {/* Đây là change, không phải reset */}
|
|
133
|
+
<input name="currentPassword" />
|
|
134
|
+
<input name="newPassword" />
|
|
135
|
+
</div>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Phương pháp detect
|
|
141
|
+
|
|
142
|
+
Rule này sử dụng **heuristic analysis** với các pattern:
|
|
143
|
+
|
|
144
|
+
1. **Context Detection**: Phát hiện ngữ cảnh password reset
|
|
145
|
+
- Keywords: `reset`, `forgot`, `recover`, `forgotpassword`
|
|
146
|
+
- Endpoints: `/reset-password`, `/forgot-password`
|
|
147
|
+
- Functions: `resetPassword()`, `forgotPassword()`
|
|
148
|
+
|
|
149
|
+
2. **Violation Detection**: Tìm yêu cầu current password
|
|
150
|
+
- Field names: `currentPassword`, `oldPassword`, `existingPassword`
|
|
151
|
+
- Validation patterns: `validateCurrentPassword()`, `checkOldPassword()`
|
|
152
|
+
- Schema fields: `currentPassword: { required: true }`
|
|
153
|
+
|
|
154
|
+
3. **Context Filtering**: Loại bỏ false positives
|
|
155
|
+
- Bỏ qua password change contexts
|
|
156
|
+
- Bỏ qua test files và documentation
|
|
157
|
+
- Bỏ qua comments và type definitions
|
|
158
|
+
|
|
159
|
+
## Tham khảo
|
|
160
|
+
|
|
161
|
+
- [OWASP A04:2021 - Insecure Design](https://owasp.org/Top10/A04_2021-Insecure_Design/)
|
|
162
|
+
- [CWE-640: Weak Password Recovery Mechanism for Forgotten Password](https://cwe.mitre.org/data/definitions/640.html)
|
|
163
|
+
- [OWASP Authentication Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html#forgot-password)
|
|
164
|
+
- [NIST SP 800-63B - Digital Identity Guidelines](https://pages.nist.gov/800-63-3/sp800-63b.html#sec5)
|
|
165
|
+
|
|
166
|
+
## Ví dụ
|
|
167
|
+
|
|
168
|
+
### Violation Examples
|
|
169
|
+
|
|
170
|
+
```javascript
|
|
171
|
+
// ❌ Express.js với current password requirement
|
|
172
|
+
app.post('/reset-password', (req, res) => {
|
|
173
|
+
if (!req.body.currentPassword) {
|
|
174
|
+
return res.status(400).json({ error: 'Current password required' });
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// ❌ NestJS với validation current password
|
|
179
|
+
@Post('reset-password')
|
|
180
|
+
async resetPassword(@Body() data: { currentPassword: string, newPassword: string }) {
|
|
181
|
+
await this.authService.validateCurrentPassword(data.currentPassword);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ❌ Mongoose schema yêu cầu current password
|
|
185
|
+
const resetSchema = new Schema({
|
|
186
|
+
currentPassword: { type: String, required: true }, // Vi phạm
|
|
187
|
+
newPassword: { type: String, required: true }
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// ❌ React form với current password field
|
|
191
|
+
<input
|
|
192
|
+
name="currentPassword"
|
|
193
|
+
placeholder="Enter current password"
|
|
194
|
+
required
|
|
195
|
+
/>
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Secure Examples
|
|
199
|
+
|
|
200
|
+
```javascript
|
|
201
|
+
// ✅ Token-based reset
|
|
202
|
+
app.post('/reset-password', (req, res) => {
|
|
203
|
+
const { token, newPassword } = req.body;
|
|
204
|
+
if (!validateResetToken(token)) {
|
|
205
|
+
return res.status(400).json({ error: 'Invalid reset token' });
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// ✅ Email-based forgot password
|
|
210
|
+
app.post('/forgot-password', (req, res) => {
|
|
211
|
+
const { email } = req.body;
|
|
212
|
+
sendResetEmail(email);
|
|
213
|
+
res.json({ message: 'Reset link sent to email' });
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// ✅ Secure reset schema
|
|
217
|
+
const resetSchema = new Schema({
|
|
218
|
+
resetToken: { type: String, required: true },
|
|
219
|
+
newPassword: { type: String, required: true },
|
|
220
|
+
tokenExpiry: { type: Date, required: true }
|
|
221
|
+
});
|
|
222
|
+
```
|