@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
|
@@ -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,150 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SunLint Category Management CLI
|
|
5
|
+
* Utility for managing categories and principles
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* node scripts/category-manager.js list
|
|
9
|
+
* node scripts/category-manager.js add <category> <principle> <description>
|
|
10
|
+
* node scripts/category-manager.js validate
|
|
11
|
+
* node scripts/category-manager.js stats
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const {
|
|
16
|
+
getValidCategories,
|
|
17
|
+
getCategoryPrinciples,
|
|
18
|
+
getCategoryDescription,
|
|
19
|
+
getCategoryStats,
|
|
20
|
+
isValidCategory,
|
|
21
|
+
addCategoryMapping
|
|
22
|
+
} = require('../core/constants/categories');
|
|
23
|
+
|
|
24
|
+
const command = process.argv[2];
|
|
25
|
+
|
|
26
|
+
switch (command) {
|
|
27
|
+
case 'list':
|
|
28
|
+
listCategories();
|
|
29
|
+
break;
|
|
30
|
+
|
|
31
|
+
case 'validate':
|
|
32
|
+
validateCategories();
|
|
33
|
+
break;
|
|
34
|
+
|
|
35
|
+
case 'stats':
|
|
36
|
+
showStats();
|
|
37
|
+
break;
|
|
38
|
+
|
|
39
|
+
case 'add':
|
|
40
|
+
addCategory(process.argv[3], process.argv[4], process.argv[5]);
|
|
41
|
+
break;
|
|
42
|
+
|
|
43
|
+
case 'check':
|
|
44
|
+
checkCategory(process.argv[3]);
|
|
45
|
+
break;
|
|
46
|
+
|
|
47
|
+
default:
|
|
48
|
+
showHelp();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function listCategories() {
|
|
52
|
+
console.log('š SunLint Categories & Principles\n');
|
|
53
|
+
|
|
54
|
+
const categories = getValidCategories();
|
|
55
|
+
categories.forEach(category => {
|
|
56
|
+
const principles = getCategoryPrinciples(category);
|
|
57
|
+
const description = getCategoryDescription(category);
|
|
58
|
+
|
|
59
|
+
console.log(`š·ļø ${category.toUpperCase()}`);
|
|
60
|
+
console.log(` Principles: ${principles.join(', ')}`);
|
|
61
|
+
console.log(` Description: ${description}`);
|
|
62
|
+
console.log('');
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function validateCategories() {
|
|
67
|
+
console.log('š Validating Category System\n');
|
|
68
|
+
|
|
69
|
+
const stats = getCategoryStats();
|
|
70
|
+
console.log(`ā
Total Categories: ${stats.totalCategories}`);
|
|
71
|
+
console.log(`ā
Total Principles: ${stats.totalPrinciples}`);
|
|
72
|
+
|
|
73
|
+
// Check for missing principles
|
|
74
|
+
const allPrinciples = Object.values(SUNLINT_PRINCIPLES);
|
|
75
|
+
const mappedPrinciples = Object.values(CATEGORY_PRINCIPLE_MAP).flat();
|
|
76
|
+
|
|
77
|
+
const missingPrinciples = allPrinciples.filter(p => !mappedPrinciples.includes(p));
|
|
78
|
+
|
|
79
|
+
if (missingPrinciples.length > 0) {
|
|
80
|
+
console.log(`ā ļø Unmapped Principles: ${missingPrinciples.join(', ')}`);
|
|
81
|
+
} else {
|
|
82
|
+
console.log('ā
All principles mapped to categories');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
console.log('\nš Category Mapping:');
|
|
86
|
+
Object.entries(CATEGORY_PRINCIPLE_MAP).forEach(([category, principles]) => {
|
|
87
|
+
console.log(` ${category} -> ${principles.join(', ')}`);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function showStats() {
|
|
92
|
+
const stats = getCategoryStats();
|
|
93
|
+
console.log('š Category Statistics\n');
|
|
94
|
+
console.log(JSON.stringify(stats, null, 2));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function addCategory(category, principle, description) {
|
|
98
|
+
if (!category || !principle || !description) {
|
|
99
|
+
console.error('ā Usage: add <category> <principle> <description>');
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
console.log(`š Adding category: ${category}`);
|
|
104
|
+
console.log(` Principle: ${principle}`);
|
|
105
|
+
console.log(` Description: ${description}`);
|
|
106
|
+
console.log('\nā ļø This would require updating category-constants.js manually');
|
|
107
|
+
console.log(' Add the following to CATEGORY_PRINCIPLE_MAP:');
|
|
108
|
+
console.log(` '${category.toLowerCase()}': ['${principle.toUpperCase()}'],`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function checkCategory(category) {
|
|
112
|
+
if (!category) {
|
|
113
|
+
console.error('ā Usage: check <category>');
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
console.log(`š Checking category: ${category}\n`);
|
|
118
|
+
|
|
119
|
+
const isValid = isValidCategory(category);
|
|
120
|
+
console.log(`Valid: ${isValid ? 'ā
' : 'ā'}`);
|
|
121
|
+
|
|
122
|
+
if (isValid) {
|
|
123
|
+
const principles = getCategoryPrinciples(category);
|
|
124
|
+
const description = getCategoryDescription(category);
|
|
125
|
+
|
|
126
|
+
console.log(`Principles: ${principles.join(', ')}`);
|
|
127
|
+
console.log(`Description: ${description}`);
|
|
128
|
+
} else {
|
|
129
|
+
console.log(`Valid categories: ${getValidCategories().join(', ')}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function showHelp() {
|
|
134
|
+
console.log(`
|
|
135
|
+
š ļø SunLint Category Manager
|
|
136
|
+
|
|
137
|
+
Commands:
|
|
138
|
+
list Show all categories and their principles
|
|
139
|
+
validate Validate the category system consistency
|
|
140
|
+
stats Show category statistics
|
|
141
|
+
check Check if a specific category is valid
|
|
142
|
+
add Add a new category (manual step required)
|
|
143
|
+
|
|
144
|
+
Examples:
|
|
145
|
+
node scripts/category-manager.js list
|
|
146
|
+
node scripts/category-manager.js check security
|
|
147
|
+
node scripts/category-manager.js validate
|
|
148
|
+
node scripts/category-manager.js add accessibility ACCESSIBILITY "Accessibility guidelines"
|
|
149
|
+
`);
|
|
150
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { SimpleRuleParser } = require('../rules/parser/rule-parser-simple');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Generate rules registry from origin-rules
|
|
9
|
+
* This script creates config/rules/rules-registry-generated.json
|
|
10
|
+
* from all *-en.md files in origin-rules/ directory
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
console.log('š Generating rules registry from origin-rules...');
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const parser = new SimpleRuleParser();
|
|
17
|
+
const originRulesDir = path.join(__dirname, '..', 'origin-rules');
|
|
18
|
+
const targetPath = path.join(__dirname, '..', 'config', 'rules', 'rules-registry-generated.json');
|
|
19
|
+
|
|
20
|
+
console.log(`Source: ${originRulesDir}`);
|
|
21
|
+
console.log(`Target: ${targetPath}`);
|
|
22
|
+
|
|
23
|
+
// Parse all rules from origin-rules
|
|
24
|
+
const allRules = parser.parseAllRules(originRulesDir);
|
|
25
|
+
|
|
26
|
+
if (allRules.length === 0) {
|
|
27
|
+
console.error('ā No rules found in origin-rules directory');
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Convert to registry format
|
|
32
|
+
const registry = {
|
|
33
|
+
rules: {}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
allRules.forEach(rule => {
|
|
37
|
+
if (rule.id) {
|
|
38
|
+
registry.rules[rule.id] = {
|
|
39
|
+
name: rule.title || `${rule.id} Rule`,
|
|
40
|
+
description: rule.description || 'No description available',
|
|
41
|
+
category: rule.category || 'quality',
|
|
42
|
+
severity: rule.severity || 'major',
|
|
43
|
+
languages: rule.language ? [rule.language] : ['All languages'],
|
|
44
|
+
version: rule.version || '1.0.0',
|
|
45
|
+
status: rule.status || 'draft',
|
|
46
|
+
tags: [rule.category || 'quality', 'readability', 'code-quality'],
|
|
47
|
+
tools: rule.tools || [],
|
|
48
|
+
framework: rule.framework || 'All',
|
|
49
|
+
principles: rule.principles || []
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Ensure target directory exists
|
|
55
|
+
const targetDir = path.dirname(targetPath);
|
|
56
|
+
if (!fs.existsSync(targetDir)) {
|
|
57
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Write registry file
|
|
61
|
+
fs.writeFileSync(targetPath, JSON.stringify(registry, null, 2), 'utf8');
|
|
62
|
+
|
|
63
|
+
const rulesCount = Object.keys(registry.rules).length;
|
|
64
|
+
const fileSize = (fs.statSync(targetPath).size / 1024).toFixed(1);
|
|
65
|
+
|
|
66
|
+
console.log(`ā
Generated registry with ${rulesCount} rules`);
|
|
67
|
+
console.log(`š File: ${targetPath} (${fileSize} KB)`);
|
|
68
|
+
console.log('');
|
|
69
|
+
console.log('š Rules by category:');
|
|
70
|
+
|
|
71
|
+
// Stats by category
|
|
72
|
+
const categories = {};
|
|
73
|
+
Object.values(registry.rules).forEach(rule => {
|
|
74
|
+
const cat = rule.category || 'unknown';
|
|
75
|
+
categories[cat] = (categories[cat] || 0) + 1;
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
Object.entries(categories)
|
|
79
|
+
.sort(([,a], [,b]) => b - a)
|
|
80
|
+
.forEach(([category, count]) => {
|
|
81
|
+
console.log(` ${category}: ${count} rules`);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
} catch (error) {
|
|
85
|
+
console.error('ā Error generating registry:', error.message);
|
|
86
|
+
console.error(error.stack);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|