@sun-asterisk/sunlint 1.3.35 → 1.3.37

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.
Files changed (103) hide show
  1. package/cli.js +33 -0
  2. package/config/rules/enhanced-rules-registry.json +354 -98
  3. package/config/rules/rules-registry-generated.json +197 -171
  4. package/core/architecture-integration.js +115 -17
  5. package/core/cli-action-handler.js +110 -26
  6. package/core/cli-program.js +14 -3
  7. package/core/github-annotate-service.js +62 -0
  8. package/core/impact-integration.js +309 -176
  9. package/core/init-command.js +227 -0
  10. package/core/output-service.js +53 -5
  11. package/core/summary-report-service.js +46 -0
  12. package/core/unified-rule-registry.js +2 -1
  13. package/engines/eslint-engine.js +6 -0
  14. package/engines/impact/core/detectors/database-detector.js +1 -1
  15. package/engines/impact/core/detectors/endpoint-detector.js +1 -1
  16. package/engines/impact/core/report-generator.js +235 -73
  17. package/origin-rules/security-en.md +470 -282
  18. package/package.json +1 -1
  19. package/rules/security/S001_backend_auth_communications/dart/analyzer.js +44 -0
  20. package/rules/security/S001_backend_auth_communications/index.js +87 -0
  21. package/rules/security/S001_backend_auth_communications/typescript/analyzer.js +164 -0
  22. package/rules/security/S002_os_command_injection/dart/analyzer.js +44 -0
  23. package/rules/security/S002_os_command_injection/index.js +87 -0
  24. package/rules/security/S002_os_command_injection/typescript/analyzer.js +194 -0
  25. package/rules/security/S008_svg_content_validation/dart/analyzer.js +44 -0
  26. package/rules/security/S008_svg_content_validation/index.js +87 -0
  27. package/rules/security/S008_svg_content_validation/typescript/analyzer.js +216 -0
  28. package/rules/security/S018_no_sensitive_browser_storage/dart/analyzer.js +44 -0
  29. package/rules/security/S018_no_sensitive_browser_storage/index.js +86 -0
  30. package/rules/security/S018_no_sensitive_browser_storage/typescript/analyzer.js +193 -0
  31. package/rules/security/S021_referrer_policy/dart/analyzer.js +44 -0
  32. package/rules/security/S021_referrer_policy/index.js +86 -0
  33. package/rules/security/S021_referrer_policy/typescript/analyzer.js +183 -0
  34. package/rules/security/S023_no_json_injection/config.json +133 -44
  35. package/rules/security/S023_no_json_injection/dart/analyzer.js +7 -6
  36. package/rules/security/S023_no_json_injection/typescript/analyzer.js +402 -126
  37. package/rules/security/S023_no_json_injection/typescript/ast-analyzer.js +571 -154
  38. package/rules/security/S026_tls_all_connections/config.json +30 -0
  39. package/rules/security/S026_tls_all_connections/typescript/analyzer.js +339 -0
  40. package/rules/security/S027_mtls_certificate_validation/config.json +30 -0
  41. package/rules/security/S027_mtls_certificate_validation/typescript/analyzer.js +225 -0
  42. package/rules/security/S035_separate_app_hostnames/config.json +28 -0
  43. package/rules/security/S035_separate_app_hostnames/typescript/analyzer.js +186 -0
  44. package/rules/security/S036_lfi_rfi_protection/config.json +2 -2
  45. package/rules/security/S039_tls_certificate_validation/config.json +29 -0
  46. package/rules/security/S039_tls_certificate_validation/typescript/analyzer.js +229 -0
  47. package/rules/security/S046_jwt_algorithm_allowlist/config.json +28 -0
  48. package/rules/security/S046_jwt_algorithm_allowlist/dart/analyzer.js +44 -0
  49. package/rules/security/S046_jwt_algorithm_allowlist/index.js +87 -0
  50. package/rules/security/S046_jwt_algorithm_allowlist/typescript/analyzer.js +235 -0
  51. package/rules/security/S047_oauth_pkce_protection/config.json +31 -0
  52. package/rules/security/S047_oauth_pkce_protection/dart/analyzer.js +44 -0
  53. package/rules/security/S047_oauth_pkce_protection/index.js +86 -0
  54. package/rules/security/S047_oauth_pkce_protection/typescript/analyzer.js +78 -0
  55. package/rules/security/S048_oauth_redirect_uri_validation/config.json +30 -0
  56. package/rules/security/S048_oauth_redirect_uri_validation/typescript/analyzer.js +278 -0
  57. package/rules/security/S049_short_validity_tokens/typescript/config.json +10 -3
  58. package/rules/security/S050_reference_tokens_entropy/config.json +28 -0
  59. package/rules/security/S050_reference_tokens_entropy/dart/analyzer.js +45 -0
  60. package/rules/security/S050_reference_tokens_entropy/index.js +86 -0
  61. package/rules/security/S050_reference_tokens_entropy/typescript/analyzer.js +74 -0
  62. package/rules/security/S053_generic_error_messages/config.json +28 -0
  63. package/rules/security/S053_generic_error_messages/dart/analyzer.js +45 -0
  64. package/rules/security/S053_generic_error_messages/index.js +86 -0
  65. package/rules/security/S053_generic_error_messages/typescript/analyzer.js +80 -0
  66. package/rules/security/S055_content_type_validation/typescript/symbol-based-analyzer.js +64 -2
  67. package/rules/security/S059_disable_debug_mode/config.json +28 -0
  68. package/rules/security/S059_disable_debug_mode/dart/analyzer.js +45 -0
  69. package/rules/security/S059_disable_debug_mode/index.js +86 -0
  70. package/rules/security/S059_disable_debug_mode/typescript/analyzer.js +85 -0
  71. package/rules/security/S060_password_minimum_length/config.json +28 -0
  72. package/rules/security/S060_password_minimum_length/dart/analyzer.js +45 -0
  73. package/rules/security/S060_password_minimum_length/index.js +86 -0
  74. package/rules/security/S060_password_minimum_length/typescript/analyzer.js +78 -0
  75. package/rules/security/S026_json_schema_validation/config.json +0 -27
  76. package/rules/security/S026_json_schema_validation/typescript/analyzer.js +0 -251
  77. package/rules/security/S027_no_hardcoded_secrets/config.json +0 -29
  78. package/rules/security/S027_no_hardcoded_secrets/typescript/analyzer.js +0 -309
  79. package/rules/security/S027_no_hardcoded_secrets/typescript/categories.json +0 -153
  80. package/rules/security/S035_path_session_cookies/config.json +0 -99
  81. package/rules/security/S035_path_session_cookies/typescript/analyzer.js +0 -316
  82. package/rules/security/S035_path_session_cookies/typescript/regex-based-analyzer.js +0 -724
  83. package/rules/security/S035_path_session_cookies/typescript/symbol-based-analyzer.js +0 -373
  84. package/rules/security/S039_no_session_tokens_in_url/config.json +0 -92
  85. package/rules/security/S039_no_session_tokens_in_url/typescript/analyzer.js +0 -262
  86. package/rules/security/S039_no_session_tokens_in_url/typescript/regex-based-analyzer.js +0 -337
  87. package/rules/security/S039_no_session_tokens_in_url/typescript/symbol-based-analyzer.js +0 -443
  88. package/rules/security/S048_no_current_password_in_reset/config.json +0 -48
  89. package/rules/security/S048_no_current_password_in_reset/typescript/analyzer.js +0 -366
  90. /package/rules/security/{S026_json_schema_validation → S026_tls_all_connections}/dart/analyzer.js +0 -0
  91. /package/rules/security/{S026_json_schema_validation → S026_tls_all_connections}/index.js +0 -0
  92. /package/rules/security/{S027_no_hardcoded_secrets → S027_mtls_certificate_validation}/dart/analyzer.js +0 -0
  93. /package/rules/security/{S027_no_hardcoded_secrets → S027_mtls_certificate_validation}/index.js +0 -0
  94. /package/rules/security/{S027_no_hardcoded_secrets → S027_mtls_certificate_validation}/typescript/categorized-analyzer.js +0 -0
  95. /package/rules/security/{S035_path_session_cookies → S035_separate_app_hostnames}/dart/analyzer.js +0 -0
  96. /package/rules/security/{S035_path_session_cookies → S035_separate_app_hostnames}/index.js +0 -0
  97. /package/rules/security/{S035_path_session_cookies → S035_separate_app_hostnames}/typescript/README.md +0 -0
  98. /package/rules/security/{S039_no_session_tokens_in_url → S039_tls_certificate_validation}/dart/analyzer.js +0 -0
  99. /package/rules/security/{S039_no_session_tokens_in_url → S039_tls_certificate_validation}/index.js +0 -0
  100. /package/rules/security/{S039_no_session_tokens_in_url → S039_tls_certificate_validation}/typescript/README.md +0 -0
  101. /package/rules/security/{S048_no_current_password_in_reset → S048_oauth_redirect_uri_validation}/dart/analyzer.js +0 -0
  102. /package/rules/security/{S048_no_current_password_in_reset → S048_oauth_redirect_uri_validation}/index.js +0 -0
  103. /package/rules/security/{S048_no_current_password_in_reset → S048_oauth_redirect_uri_validation}/typescript/README.md +0 -0
@@ -0,0 +1,78 @@
1
+ /**
2
+ * S060 – Enforce minimum password length of 8 characters, recommend 15+
3
+ *
4
+ * Detects weak password length requirements:
5
+ * - Minimum length < 8
6
+ * - No length validation
7
+ */
8
+
9
+ const fs = require('fs');
10
+
11
+ class S060Analyzer {
12
+ constructor() {
13
+ this.ruleId = 'S060';
14
+ this.ruleName = 'Enforce minimum password length of 8 characters';
15
+ this.description = 'Password minimum length should be 8+ characters';
16
+
17
+ this.warningPatterns = [
18
+ {
19
+ pattern: /password.*length\s*[<>=!]+\s*[1-7]\b/gi,
20
+ message: 'Password minimum length below 8 characters - NIST recommends 8+ minimum',
21
+ type: 'weak_min_length'
22
+ },
23
+ {
24
+ pattern: /minLength\s*:\s*[1-7]\b.*password/gi,
25
+ message: 'Password minLength below 8 - increase to at least 8',
26
+ type: 'weak_minlength_config'
27
+ },
28
+ {
29
+ pattern: /password.*min\s*:\s*[1-7]\b/gi,
30
+ message: 'Password min length below 8 - increase to at least 8',
31
+ type: 'weak_min_config'
32
+ }
33
+ ];
34
+
35
+ this.skipPatterns = [
36
+ /\.test\./i, /\.spec\./i, /test\//i, /__tests__\//i, /node_modules/i
37
+ ];
38
+ }
39
+
40
+ shouldSkipFile(filePath) {
41
+ return this.skipPatterns.some(p => p.test(filePath));
42
+ }
43
+
44
+ isPasswordRelated(content) {
45
+ return /password|passwd|pwd/i.test(content);
46
+ }
47
+
48
+ async analyze(files, language, options = {}) {
49
+ const violations = [];
50
+ for (const filePath of files) {
51
+ if (this.shouldSkipFile(filePath)) continue;
52
+ try {
53
+ const content = fs.readFileSync(filePath, 'utf8');
54
+ if (!this.isPasswordRelated(content)) continue;
55
+ violations.push(...this.analyzeFile(content, filePath));
56
+ } catch (e) { /* skip */ }
57
+ }
58
+ return violations;
59
+ }
60
+
61
+ analyzeFile(content, filePath) {
62
+ const violations = [];
63
+ for (const { pattern, message, type } of this.warningPatterns) {
64
+ pattern.lastIndex = 0;
65
+ let match;
66
+ while ((match = pattern.exec(content)) !== null) {
67
+ const lineNum = content.substring(0, match.index).split('\n').length;
68
+ violations.push({
69
+ file: filePath, line: lineNum, column: 1,
70
+ message, severity: 'warning', ruleId: this.ruleId, type
71
+ });
72
+ }
73
+ }
74
+ return violations;
75
+ }
76
+ }
77
+
78
+ module.exports = S060Analyzer;
@@ -1,27 +0,0 @@
1
- {
2
- "id": "S026",
3
- "name": "JSON Schema Validation for Input Data",
4
- "description": "Ensure all user input data (from HTTP requests, APIs) is validated using JSON schemas before processing to prevent injection attacks.",
5
- "category": "security",
6
- "severity": "warning",
7
- "enabled": true,
8
- "engines": ["heuristic"],
9
- "enginePreference": ["heuristic"],
10
- "tags": ["security", "validation", "input", "json-schema", "http"],
11
- "examples": {
12
- "valid": [
13
- "const schema = joi.object({ name: joi.string() }); const { error } = schema.validate(req.body);",
14
- "const ajv = new Ajv(); const valid = ajv.validate(schema, req.body);",
15
- "const styles = { body: { color: 'red' } }; // Style object - OK"
16
- ],
17
- "invalid": [
18
- "const data = req.body; processUser(data); // No validation",
19
- "const query = req.query; database.find(query); // Direct usage without validation"
20
- ]
21
- },
22
- "fixable": false,
23
- "docs": {
24
- "description": "This rule ensures that all user input data from HTTP requests is validated using JSON schemas before processing. Direct usage of req.body, req.query, req.params without validation can lead to injection attacks and data corruption.",
25
- "url": "https://owasp.org/Top10/A03_2021-Injection/"
26
- }
27
- }
@@ -1,251 +0,0 @@
1
- /**
2
- * Heuristic analyzer for: S026 – JSON Schema Validation cho dữ liệu đầu vào
3
- * Purpose: Detect unvalidated JSON inputs while avoiding false positives on styles/config objects
4
- */
5
-
6
- class S026Analyzer {
7
- constructor() {
8
- this.ruleId = 'S026';
9
- this.ruleName = 'JSON Schema Validation Required';
10
- this.description = 'Áp dụng JSON Schema Validation cho dữ liệu đầu vào để đảm bảo an toàn';
11
-
12
- // Patterns that indicate actual HTTP/API input (should be validated)
13
- this.httpInputPatterns = [
14
- 'req.body', 'req.query', 'request.body', 'request.query',
15
- 'ctx.body', 'ctx.query', 'context.body', 'context.query',
16
- 'event.body', 'event.queryStringParameters'
17
- ];
18
-
19
- // Patterns that are NOT JSON inputs (should not be flagged)
20
- this.nonInputPatterns = [
21
- 'styles.', 'css.', 'theme.', 'colors.',
22
- 'config.', 'settings.', 'options.',
23
- 'data.', 'props.', 'state.',
24
- 'const.', 'static.', 'default.'
25
- ];
26
-
27
- // Validation patterns that indicate input is being validated
28
- this.validationPatterns = [
29
- 'schema.validate', 'joi.validate', 'ajv.validate',
30
- 'validate(', 'validateInput(', 'validateBody(',
31
- 'isValid(', 'checkSchema(', 'parseSchema(',
32
- '.validate(', '.valid(', '.check('
33
- ];
34
-
35
- // Express/HTTP framework patterns
36
- this.httpFrameworkPatterns = [
37
- 'app.post(', 'app.put(', 'app.patch(',
38
- 'router.post(', 'router.put(', 'router.patch(',
39
- 'express()', '.post(', '.put(', '.patch('
40
- ];
41
- }
42
-
43
- async analyze(files, language, options = {}) {
44
- const violations = [];
45
-
46
- for (const filePath of files) {
47
- if (options.verbose) {
48
- console.log(`🔍 Running S026 analysis on ${require('path').basename(filePath)}`);
49
- }
50
-
51
- try {
52
- const content = require('fs').readFileSync(filePath, 'utf8');
53
- const fileViolations = this.analyzeFile(content, filePath);
54
- violations.push(...fileViolations);
55
- } catch (error) {
56
- console.warn(`⚠️ Failed to analyze ${filePath}: ${error.message}`);
57
- }
58
- }
59
-
60
- return violations;
61
- }
62
-
63
- analyzeFile(content, filePath) {
64
- const violations = [];
65
- const lines = content.split('\n');
66
-
67
- // Find all potential JSON inputs
68
- const potentialInputs = this.findPotentialInputs(lines);
69
-
70
- // Check if they're validated
71
- const validatedInputs = this.findValidatedInputs(content);
72
-
73
- // Report unvalidated inputs
74
- potentialInputs.forEach(input => {
75
- if (!this.isInputValidated(input, validatedInputs) &&
76
- this.isActualJSONInput(input, content)) {
77
- violations.push({
78
- file: filePath,
79
- line: input.line,
80
- column: input.column,
81
- message: `JSON input '${input.expression}' should be validated using a JSON schema before use. Consider using schema.validate(), joi.validate(), or similar validation library.`,
82
- severity: 'warning',
83
- ruleId: this.ruleId,
84
- type: 'unvalidated_json_input',
85
- inputExpression: input.expression
86
- });
87
- }
88
- });
89
-
90
- return violations;
91
- }
92
-
93
- findPotentialInputs(lines) {
94
- const inputs = [];
95
-
96
- lines.forEach((line, index) => {
97
- const trimmedLine = line.trim();
98
-
99
- // Skip comments and imports
100
- if (trimmedLine.startsWith('//') || trimmedLine.startsWith('/*') ||
101
- trimmedLine.startsWith('import') || trimmedLine.startsWith('export')) {
102
- return;
103
- }
104
-
105
- // Look for .body or .query patterns
106
- const bodyMatches = [...line.matchAll(/(\w+\.\w*body\w*)/g)];
107
- const queryMatches = [...line.matchAll(/(\w+\.\w*query\w*)/g)];
108
-
109
- [...bodyMatches, ...queryMatches].forEach(match => {
110
- const expression = match[1];
111
- const column = match.index + 1;
112
-
113
- inputs.push({
114
- expression,
115
- line: index + 1,
116
- column,
117
- originalLine: line
118
- });
119
- });
120
- });
121
-
122
- return inputs;
123
- }
124
-
125
- findValidatedInputs(content) {
126
- const validatedInputs = new Set();
127
-
128
- // Find validation calls
129
- this.validationPatterns.forEach(pattern => {
130
- const regex = new RegExp(pattern.replace('(', '\\(') + '\\s*\\(([^)]+)\\)', 'g');
131
- let match;
132
-
133
- while ((match = regex.exec(content)) !== null) {
134
- const validatedInput = match[1].trim();
135
- validatedInputs.add(validatedInput);
136
-
137
- // Also add simplified version (e.g., req.body from schema.validate(req.body))
138
- const simplifiedInput = validatedInput.replace(/^\w+\./, '').replace(/\s+/g, '');
139
- if (simplifiedInput.includes('.')) {
140
- validatedInputs.add(simplifiedInput);
141
- }
142
- }
143
- });
144
-
145
- return validatedInputs;
146
- }
147
-
148
- isInputValidated(input, validatedInputs) {
149
- const expression = input.expression;
150
-
151
- // Check exact match
152
- if (validatedInputs.has(expression)) {
153
- return true;
154
- }
155
-
156
- // Check if any validated input contains this expression
157
- for (const validated of validatedInputs) {
158
- if (validated.includes(expression) || expression.includes(validated)) {
159
- return true;
160
- }
161
- }
162
-
163
- // Check if validation happens in the same line or nearby
164
- const lineContent = input.originalLine.toLowerCase();
165
- if (this.validationPatterns.some(pattern => lineContent.includes(pattern.toLowerCase()))) {
166
- return true;
167
- }
168
-
169
- return false;
170
- }
171
-
172
- isActualJSONInput(input, content) {
173
- const expression = input.expression.toLowerCase();
174
-
175
- // Skip known non-input patterns (user feedback - styles, config, etc.)
176
- if (this.nonInputPatterns.some(pattern => expression.startsWith(pattern.toLowerCase()))) {
177
- return false;
178
- }
179
-
180
- // Skip React/CSS style objects
181
- if (this.isStyleOrConfigObject(input, content)) {
182
- return false;
183
- }
184
-
185
- // Check if it matches HTTP input patterns
186
- if (this.httpInputPatterns.some(pattern => expression.includes(pattern.toLowerCase()))) {
187
- return true;
188
- }
189
-
190
- // Check if it's in HTTP handler context
191
- if (this.isInHTTPHandlerContext(input, content)) {
192
- return true;
193
- }
194
-
195
- // Default to false to avoid false positives
196
- return false;
197
- }
198
-
199
- isStyleOrConfigObject(input, content) {
200
- const expression = input.expression;
201
- const lineContent = input.originalLine.toLowerCase();
202
-
203
- // Check for React/CSS style usage patterns
204
- const styleIndicators = [
205
- 'style=', 'css=', 'theme=', 'styles=',
206
- 'background', 'color:', 'font', 'margin:', 'padding:',
207
- 'import', 'const styles', 'const css', 'const theme'
208
- ];
209
-
210
- if (styleIndicators.some(indicator => lineContent.includes(indicator))) {
211
- return true;
212
- }
213
-
214
- // Check context around the input for style/config patterns
215
- const lines = content.split('\n');
216
- const inputLineIndex = input.line - 1;
217
- const contextStart = Math.max(0, inputLineIndex - 3);
218
- const contextEnd = Math.min(lines.length, inputLineIndex + 3);
219
- const contextLines = lines.slice(contextStart, contextEnd).join('\n').toLowerCase();
220
-
221
- const contextIndicators = [
222
- 'const styles', 'const css', 'const config', 'const theme',
223
- 'styleshet.create', 'react', 'jsx', '<div', '</div>', 'component',
224
- 'export default', 'props', 'state'
225
- ];
226
-
227
- return contextIndicators.some(indicator => contextLines.includes(indicator));
228
- }
229
-
230
- isInHTTPHandlerContext(input, content) {
231
- const lines = content.split('\n');
232
- const inputLineIndex = input.line - 1;
233
-
234
- // Check surrounding context for HTTP framework patterns
235
- const contextStart = Math.max(0, inputLineIndex - 10);
236
- const contextEnd = Math.min(lines.length, inputLineIndex + 5);
237
- const contextLines = lines.slice(contextStart, contextEnd).join('\n').toLowerCase();
238
-
239
- // Look for HTTP handler patterns in context
240
- const httpIndicators = [
241
- 'app.post', 'app.put', 'app.patch', 'app.delete',
242
- 'router.post', 'router.put', 'router.patch',
243
- '(req, res)', 'request, response', 'ctx.body', 'ctx.query',
244
- 'express', 'fastify', 'koa', 'hapi'
245
- ];
246
-
247
- return httpIndicators.some(indicator => contextLines.includes(indicator));
248
- }
249
- }
250
-
251
- module.exports = S026Analyzer;
@@ -1,29 +0,0 @@
1
- {
2
- "id": "S027",
3
- "name": "No Hardcoded Secrets",
4
- "description": "Prevent hardcoded passwords, API keys, secrets while avoiding false positives on state variables and configuration.",
5
- "category": "security",
6
- "severity": "warning",
7
- "enabled": true,
8
- "engines": ["heuristic"],
9
- "enginePreference": ["heuristic"],
10
- "tags": ["security", "secrets", "credentials", "api-keys"],
11
- "examples": {
12
- "valid": [
13
- "const password = process.env.PASSWORD;",
14
- "const _isEnablePassCode = useState(false);",
15
- "const passwordFieldVisible = true;",
16
- "const routes = { setupPassword: '/setup-password' };"
17
- ],
18
- "invalid": [
19
- "const password = 'admin123';",
20
- "const apiKey = 'sk-1234567890abcdef';",
21
- "const secret = 'my-secret-token';"
22
- ]
23
- },
24
- "fixable": false,
25
- "docs": {
26
- "description": "This rule prevents hardcoded sensitive information like passwords, API keys, and secrets in source code. It avoids false positives on state variables, route names, and input type configurations.",
27
- "url": "https://owasp.org/Top10/A02_2021-Cryptographic_Failures/"
28
- }
29
- }
@@ -1,309 +0,0 @@
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
- // Skip test files completely - they often contain mock/fake credentials
79
- const testPatterns = [
80
- /\.spec\.(ts|tsx|js|jsx)$/,
81
- /\.test\.(ts|tsx|js|jsx)$/,
82
- /__tests__\//,
83
- /__mocks__\//,
84
- /\/tests?\//,
85
- /\/fixtures?\//,
86
- /\/factories\//, // Test factories
87
- /setupTests\./,
88
- /testSetup\./,
89
- ];
90
-
91
- if (testPatterns.some(pattern => pattern.test(filePath))) {
92
- return true;
93
- }
94
-
95
- return skipPatterns.some(pattern => filePath.includes(pattern));
96
- }
97
-
98
- analyzeFile(content, filePath) {
99
- const violations = [];
100
- // Handle different line endings (Windows \r\n, Unix \n, Mac \r)
101
- const lines = content.split(/\r?\n/);
102
-
103
- // Check if this is a test file for context
104
- const isTestFile = this.isTestFile(filePath);
105
-
106
- lines.forEach((line, index) => {
107
- const lineNumber = index + 1;
108
- const trimmedLine = line.trim();
109
-
110
- // Skip comments and imports
111
- if (this.isCommentOrImport(trimmedLine)) {
112
- return;
113
- }
114
-
115
- // Skip NEXT_PUBLIC_ environment variables - these are public by design
116
- if (this.isPublicEnvironmentVariable(line)) {
117
- return;
118
- }
119
-
120
- // Check global exclude patterns first
121
- if (this.matchesGlobalExcludes(line)) {
122
- return;
123
- }
124
-
125
- // Check each category
126
- this.categories.forEach(category => {
127
- const categoryViolations = this.checkCategory(
128
- category, line, lineNumber, filePath, isTestFile
129
- );
130
- violations.push(...categoryViolations);
131
- });
132
- });
133
-
134
- return violations;
135
- }
136
-
137
- isTestFile(filePath) {
138
- const testPatterns = [
139
- /\.(test|spec)\./i,
140
- /__tests__/i,
141
- /\/tests?\//i,
142
- /\/spec\//i,
143
- /setupTests/i,
144
- /testSetup/i,
145
- /test[-_]/i, // Matches test- or test_
146
- /^.*\/test[^\/]*\.js$/i // Matches files starting with test
147
- ];
148
-
149
- return testPatterns.some(pattern => pattern.test(filePath));
150
- }
151
-
152
- isCommentOrImport(line) {
153
- return line.startsWith('//') || line.startsWith('/*') ||
154
- line.startsWith('import') || line.startsWith('export') ||
155
- line.startsWith('*') || line.startsWith('<');
156
- }
157
-
158
- matchesGlobalExcludes(line) {
159
- return this.globalExcludePatterns.some(pattern => pattern.test(line));
160
- }
161
-
162
- isPublicEnvironmentVariable(line) {
163
- // NEXT_PUBLIC_, REACT_APP_, VITE_, PUBLIC_ prefixed env vars are public by design
164
- // These are intentionally exposed to the client-side and are not secrets
165
- const publicEnvPatterns = [
166
- /NEXT_PUBLIC_[A-Z0-9_]+\s*[:=]/i, // Next.js public env vars
167
- /REACT_APP_[A-Z0-9_]+\s*[:=]/i, // Create React App public env vars
168
- /VITE_[A-Z0-9_]+\s*[:=]/i, // Vite public env vars
169
- /PUBLIC_[A-Z0-9_]+\s*[:=]/i, // Generic public prefix
170
- /EXPO_PUBLIC_[A-Z0-9_]+\s*[:=]/i, // Expo public env vars
171
- ];
172
-
173
- return publicEnvPatterns.some(pattern => pattern.test(line));
174
- }
175
-
176
- isEnvironmentVariableUsage(line) {
177
- // Patterns that indicate environment variable or config service usage - these are SAFE
178
- const safePatterns = [
179
- /process\.env\./i, // process.env.DB_PASSWORD
180
- /configService\.get/i, // configService.get('DB_PASSWORD')
181
- /config\.get/i, // config.get('API_KEY')
182
- /getConfig\(/i, // getConfig('secret')
183
- /env\(['"]([^'"]+)['"]\)/i, // env('SECRET_KEY')
184
- /process\.env\[['"]([^'"]+)['"]\]/i, // process.env['API_KEY']
185
- /import\.meta\.env\./i, // import.meta.env.VITE_API_KEY
186
- /Deno\.env\.get/i, // Deno.env.get()
187
- /os\.getenv/i, // Python os.getenv()
188
- /System\.getenv/i, // Java System.getenv()
189
- ];
190
-
191
- return safePatterns.some(pattern => pattern.test(line));
192
- }
193
-
194
- checkCategory(category, line, lineNumber, filePath, isTestFile) {
195
- const violations = [];
196
-
197
- category.compiledPatterns.forEach(({ regex, original }) => {
198
- let match;
199
-
200
- // Reset regex lastIndex for global patterns
201
- regex.lastIndex = 0;
202
-
203
- while ((match = regex.exec(line)) !== null) {
204
- const matchedText = match[0];
205
- const column = match.index + 1;
206
-
207
- // Check length constraints
208
- if (matchedText.length < this.minLength || matchedText.length > this.maxLength) {
209
- continue;
210
- }
211
-
212
- // Check category-specific excludes
213
- if (category.compiledExcludePatterns &&
214
- category.compiledExcludePatterns.some(pattern => pattern.test(matchedText))) {
215
- continue;
216
- }
217
-
218
- // Check if this uses environment variables or config services - SAFE patterns
219
- if (this.isEnvironmentVariableUsage(line)) {
220
- continue;
221
- }
222
-
223
- // Be more lenient in test files - skip all but critical severity
224
- // Test files commonly use hardcoded values for testing purposes
225
- if (isTestFile && (category.severity === 'low' || category.severity === 'medium' || category.severity === 'high')) {
226
- continue;
227
- }
228
-
229
- violations.push({
230
- file: filePath,
231
- line: lineNumber,
232
- column: column,
233
- message: `[${category.name}] Potential ${category.severity} security risk: '${matchedText}'. ${category.description}`,
234
- severity: this.mapSeverity(category.severity),
235
- ruleId: this.ruleId,
236
- category: category.name,
237
- categoryDescription: category.description,
238
- matchedPattern: original,
239
- matchedText: matchedText
240
- });
241
- }
242
- });
243
-
244
- return violations;
245
- }
246
-
247
- mapSeverity(categorySeverity) {
248
- const severityMap = {
249
- 'critical': 'error',
250
- 'high': 'warning',
251
- 'medium': 'warning',
252
- 'low': 'info'
253
- };
254
-
255
- return severityMap[categorySeverity] || 'warning';
256
- }
257
-
258
- // Method for getting category statistics
259
- getCategoryStats(violations) {
260
- const stats = {};
261
-
262
- violations.forEach(violation => {
263
- const category = violation.category;
264
- if (!stats[category]) {
265
- stats[category] = {
266
- count: 0,
267
- severity: violation.severity,
268
- files: new Set()
269
- };
270
- }
271
- stats[category].count++;
272
- stats[category].files.add(violation.file);
273
- });
274
-
275
- // Convert Set to array for JSON serialization
276
- Object.keys(stats).forEach(category => {
277
- stats[category].files = Array.from(stats[category].files);
278
- stats[category].fileCount = stats[category].files.length;
279
- });
280
-
281
- return stats;
282
- }
283
-
284
- // Method for filtering by category
285
- filterByCategory(violations, categoryNames) {
286
- if (!categoryNames || categoryNames.length === 0) {
287
- return violations;
288
- }
289
-
290
- return violations.filter(violation =>
291
- categoryNames.includes(violation.category)
292
- );
293
- }
294
-
295
- // Method for filtering by severity
296
- filterBySeverity(violations, minSeverity = 'info') {
297
- const severityOrder = ['info', 'warning', 'error'];
298
- const minIndex = severityOrder.indexOf(minSeverity);
299
-
300
- if (minIndex === -1) return violations;
301
-
302
- return violations.filter(violation => {
303
- const violationIndex = severityOrder.indexOf(violation.severity);
304
- return violationIndex >= minIndex;
305
- });
306
- }
307
- }
308
-
309
- module.exports = S027CategorizedAnalyzer;