@sun-asterisk/sunlint 1.3.6 → 1.3.7

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.
@@ -0,0 +1,46 @@
1
+ {
2
+ "id": "C073",
3
+ "name": "C073_validate_required_config_on_startup",
4
+ "category": "configuration",
5
+ "description": "C073 - Validate mandatory configuration at startup and fail fast on invalid/missing values.",
6
+ "severity": "error",
7
+ "enabled": true,
8
+ "semantic": { "enabled": true, "priority": "high", "fallback": "heuristic" },
9
+ "patterns": {
10
+ "include": ["**/*.ts","**/*.tsx","**/*.js","**/*.jsx","**/*.java","**/*.go"],
11
+ "exclude": ["**/*.test.*","**/*.spec.*","**/__tests__/**","**/fixtures/**","**/examples/**"]
12
+ },
13
+ "options": {
14
+ "configModules": {
15
+ "typescript": ["src/config/**","**/config/**","**/bootstrap/**","**/main.ts"],
16
+ "java": ["**/config/**","**/Configuration/**","**/Application.java","**/*Application.java"],
17
+ "go": ["cmd/**","**/config/**","**/main.go"]
18
+ },
19
+ "envAccessors": {
20
+ "typescript": ["process.env.*"],
21
+ "java": ["System.getenv(*)","System.getProperty(*)"],
22
+ "go": ["os.Getenv(*)"]
23
+ },
24
+ "schemaDetectors": {
25
+ "typescript": ["zod","joi","yup","envalid","dotenv-safe","class-validator"],
26
+ "java": ["@ConfigurationProperties","@Validated","jakarta.validation","hibernate.validator"],
27
+ "go": ["github.com/kelseyhightower/envconfig","github.com/spf13/viper"]
28
+ },
29
+ "failFastSignals": {
30
+ "typescript": ["throw new Error(*)","process.exit(1)"],
31
+ "java": ["throw new RuntimeException(*)","SpringApplication.exit(*)","System.exit(1)"],
32
+ "go": ["log.Fatal(*)","panic(*)","os.Exit(1)"]
33
+ },
34
+ "dangerousDefaults": ["|| ''","|| 0","|| 'http://localhost'","?: ''","?: 0"],
35
+ "thresholds": {
36
+ "maxEnvReadsOutsideConfig": 3
37
+ },
38
+ "policy": {
39
+ "requireSchemaOrExplicitChecks": true,
40
+ "requireFailFast": true,
41
+ "forbidEnvReadsOutsideConfig": true,
42
+ "flagDangerousDefaults": true,
43
+ "requireStartupConnectivityChecks": true
44
+ }
45
+ }
46
+ }
@@ -0,0 +1,370 @@
1
+ const { traverse } = require('@babel/traverse');
2
+ const t = require('@babel/types');
3
+
4
+ class C073SymbolBasedAnalyzer {
5
+ constructor(options = {}) {
6
+ this.options = options;
7
+ this.configModules = options.configModules || {};
8
+ this.envAccessors = options.envAccessors || {};
9
+ this.schemaDetectors = options.schemaDetectors || {};
10
+ this.failFastSignals = options.failFastSignals || {};
11
+ this.dangerousDefaults = options.dangerousDefaults || [];
12
+ this.policy = options.policy || {};
13
+ }
14
+
15
+ analyze(ast, filePath, content) {
16
+ const analysis = {
17
+ envAccess: [],
18
+ schemaValidation: [],
19
+ failFastMechanisms: [],
20
+ dangerousDefaults: [],
21
+ connectivityChecks: [],
22
+ imports: [],
23
+ configValidation: false,
24
+ hasFailFast: false
25
+ };
26
+
27
+ const language = this.detectLanguageFromPath(filePath);
28
+
29
+ // Analyze both config files and service files for different patterns
30
+ const isConfigFile = this.isConfigOrStartupFile(filePath, language);
31
+
32
+ // Traverse AST to collect information
33
+ traverse(ast, {
34
+ // Track imports for schema validation libraries
35
+ ImportDeclaration: (path) => {
36
+ const source = path.node.source.value;
37
+ analysis.imports.push(source);
38
+
39
+ // Check for schema validation libraries
40
+ if (this.isSchemaValidationLibrary(source, language)) {
41
+ analysis.schemaValidation.push({
42
+ library: source,
43
+ line: path.node.loc?.start.line || 1
44
+ });
45
+ }
46
+ },
47
+
48
+ // Track environment variable access
49
+ MemberExpression: (path) => {
50
+ if (this.isEnvAccess(path.node, language)) {
51
+ const envProperty = this.getEnvProperty(path.node);
52
+ analysis.envAccess.push({
53
+ variable: envProperty,
54
+ match: `process.env.${envProperty}`,
55
+ line: path.node.loc?.start.line || 1,
56
+ hasDefault: this.hasDefaultValue(path.parent)
57
+ });
58
+ }
59
+ },
60
+
61
+ // Track call expressions for multiple purposes
62
+ CallExpression: (path) => {
63
+ const node = path.node;
64
+
65
+ // Check for require statements
66
+ if (t.isIdentifier(node.callee, { name: 'require' })) {
67
+ const source = node.arguments[0]?.value;
68
+ if (source && this.isSchemaValidationLibrary(source, language)) {
69
+ analysis.schemaValidation.push({
70
+ library: source,
71
+ line: node.loc?.start.line || 1
72
+ });
73
+ }
74
+ }
75
+
76
+ // Check for fail-fast calls (process.exit, throw, etc.)
77
+ if (this.isFailFastCall(node, language)) {
78
+ analysis.failFastMechanisms.push({
79
+ type: this.getFailFastType(node),
80
+ line: node.loc?.start.line || 1
81
+ });
82
+ }
83
+
84
+ // Check for connectivity patterns (ping, connect, health check)
85
+ if (this.isConnectivityCheck(node)) {
86
+ analysis.connectivityChecks.push({
87
+ method: this.getConnectivityMethod(node),
88
+ line: node.loc?.start.line || 1
89
+ });
90
+ }
91
+ },
92
+
93
+ // Track dangerous default patterns
94
+ LogicalExpression: (path) => {
95
+ if (this.isDangerousDefault(path.node)) {
96
+ analysis.dangerousDefaults.push({
97
+ operator: path.node.operator,
98
+ pattern: this.getDangerousDefaultPattern(path.node),
99
+ line: path.node.loc?.start.line || 1
100
+ });
101
+ }
102
+ },
103
+
104
+ // Track conditional expressions with dangerous defaults
105
+ ConditionalExpression: (path) => {
106
+ if (this.isDangerousConditionalDefault(path.node)) {
107
+ analysis.dangerousDefaults.push({
108
+ type: 'conditional',
109
+ pattern: this.getDangerousDefaultPattern(path.node),
110
+ line: path.node.loc?.start.line || 1
111
+ });
112
+ }
113
+ },
114
+
115
+ // Check for validation patterns
116
+ IfStatement: (path) => {
117
+ if (this.isConfigValidation(path.node, analysis.envAccess)) {
118
+ analysis.configValidation = true;
119
+ }
120
+ },
121
+
122
+ // Check for throw statements with configuration errors
123
+ ThrowStatement: (path) => {
124
+ if (this.isConfigRelatedError(path.node)) {
125
+ analysis.failFastMechanisms.push({
126
+ type: 'throw',
127
+ line: path.node.loc?.start.line || 1
128
+ });
129
+ }
130
+ }
131
+ });
132
+
133
+ return analysis;
134
+ }
135
+
136
+ detectLanguageFromPath(filePath) {
137
+ const ext = filePath.split('.').pop()?.toLowerCase();
138
+ if (['ts', 'tsx', 'js', 'jsx'].includes(ext)) return 'typescript';
139
+ if (['java'].includes(ext)) return 'java';
140
+ if (['go'].includes(ext)) return 'go';
141
+ return null;
142
+ }
143
+
144
+ isConfigOrStartupFile(filePath, language) {
145
+ const configPatterns = this.configModules[language] || [];
146
+ return configPatterns.some(pattern => {
147
+ const globPattern = pattern.replace(/\*\*/g, '.*').replace(/\*/g, '[^/]*');
148
+ const regex = new RegExp(globPattern.replace(/\//g, '\\/'));
149
+ return regex.test(filePath);
150
+ });
151
+ }
152
+
153
+ isSchemaValidationLibrary(source, language) {
154
+ const libraries = this.schemaDetectors[language] || [];
155
+ return libraries.some(lib => source.includes(lib));
156
+ }
157
+
158
+ isEnvAccess(node, language) {
159
+ if (language === 'typescript') {
160
+ // process.env.VARIABLE
161
+ return (
162
+ t.isMemberExpression(node) &&
163
+ t.isMemberExpression(node.object) &&
164
+ t.isIdentifier(node.object.object, { name: 'process' }) &&
165
+ t.isIdentifier(node.object.property, { name: 'env' })
166
+ );
167
+ }
168
+ return false;
169
+ }
170
+
171
+ getEnvProperty(node) {
172
+ if (t.isIdentifier(node.property)) {
173
+ return node.property.name;
174
+ }
175
+ if (t.isStringLiteral(node.property)) {
176
+ return node.property.value;
177
+ }
178
+ return 'unknown';
179
+ }
180
+
181
+ hasDefaultValue(parent) {
182
+ return (
183
+ t.isLogicalExpression(parent) ||
184
+ t.isConditionalExpression(parent) ||
185
+ (t.isAssignmentExpression(parent) && parent.right)
186
+ );
187
+ }
188
+
189
+ isFailFastCall(node, language) {
190
+ if (language === 'typescript') {
191
+ // process.exit(1)
192
+ if (
193
+ t.isMemberExpression(node.callee) &&
194
+ t.isIdentifier(node.callee.object, { name: 'process' }) &&
195
+ t.isIdentifier(node.callee.property, { name: 'exit' })
196
+ ) {
197
+ return true;
198
+ }
199
+ }
200
+ return false;
201
+ }
202
+
203
+ getFailFastType(node) {
204
+ if (t.isMemberExpression(node.callee)) {
205
+ if (t.isIdentifier(node.callee.property, { name: 'exit' })) {
206
+ return 'process.exit';
207
+ }
208
+ }
209
+ if (t.isThrowStatement(node)) {
210
+ return 'throw';
211
+ }
212
+ return 'unknown';
213
+ }
214
+
215
+ isConnectivityCheck(node) {
216
+ if (!t.isCallExpression(node)) return false;
217
+
218
+ // Check for common connectivity patterns
219
+ const patterns = [
220
+ 'ping', 'connect', 'healthCheck', 'testConnection',
221
+ 'validateConnection', 'checkHealth', 'authenticate'
222
+ ];
223
+
224
+ if (t.isMemberExpression(node.callee)) {
225
+ const methodName = node.callee.property?.name;
226
+ return patterns.includes(methodName);
227
+ }
228
+
229
+ if (t.isIdentifier(node.callee)) {
230
+ return patterns.includes(node.callee.name);
231
+ }
232
+
233
+ return false;
234
+ }
235
+
236
+ getConnectivityMethod(node) {
237
+ if (t.isMemberExpression(node.callee)) {
238
+ return node.callee.property?.name || 'unknown';
239
+ }
240
+ if (t.isIdentifier(node.callee)) {
241
+ return node.callee.name;
242
+ }
243
+ return 'unknown';
244
+ }
245
+
246
+ isDangerousDefault(node) {
247
+ if (!t.isLogicalExpression(node, { operator: '||' })) {
248
+ return false;
249
+ }
250
+
251
+ const rightValue = this.getDefaultValue(node.right);
252
+ const dangerousPatterns = [
253
+ "''", '""', '0', 'null', 'undefined',
254
+ "'localhost'", "'http://localhost'", "'dev'", "'development'"
255
+ ];
256
+
257
+ return dangerousPatterns.includes(rightValue);
258
+ }
259
+
260
+ isDangerousConditionalDefault(node) {
261
+ if (!t.isConditionalExpression(node)) return false;
262
+
263
+ const consequent = this.getDefaultValue(node.consequent);
264
+ const alternate = this.getDefaultValue(node.alternate);
265
+
266
+ const dangerousPatterns = [
267
+ "''", '""', '0', 'null', 'undefined',
268
+ "'localhost'", "'http://localhost'", "'dev'", "'development'"
269
+ ];
270
+
271
+ return dangerousPatterns.includes(consequent) || dangerousPatterns.includes(alternate);
272
+ }
273
+
274
+ getDangerousDefaultPattern(node) {
275
+ if (t.isLogicalExpression(node)) {
276
+ return this.getDefaultValue(node.right);
277
+ }
278
+ if (t.isConditionalExpression(node)) {
279
+ const consequent = this.getDefaultValue(node.consequent);
280
+ const alternate = this.getDefaultValue(node.alternate);
281
+ return `${consequent} ? ${alternate}`;
282
+ }
283
+ return 'unknown';
284
+ }
285
+
286
+ getDefaultValue(node) {
287
+ if (t.isStringLiteral(node)) {
288
+ return `'${node.value}'`;
289
+ }
290
+ if (t.isNumericLiteral(node)) {
291
+ return node.value.toString();
292
+ }
293
+ if (t.isNullLiteral(node)) {
294
+ return 'null';
295
+ }
296
+ if (t.isIdentifier(node, { name: 'undefined' })) {
297
+ return 'undefined';
298
+ }
299
+ return 'unknown';
300
+ }
301
+
302
+ isConfigValidation(node, envAccess) {
303
+ // Simple heuristic: if an if statement contains a check for environment variables
304
+ // and has a throw or process.exit in the consequent, it's likely validation
305
+ if (t.isIfStatement(node)) {
306
+ const test = node.test;
307
+ const consequent = node.consequent;
308
+
309
+ // Check if test involves environment variables
310
+ const involvesEnv = this.containsEnvAccess(test);
311
+
312
+ // Check if consequent has fail-fast behavior
313
+ const hasFailFast = this.containsFailFast(consequent);
314
+
315
+ return involvesEnv && hasFailFast;
316
+ }
317
+
318
+ return false;
319
+ }
320
+
321
+ containsEnvAccess(node) {
322
+ // Simple traversal to check if node contains process.env access
323
+ let hasEnvAccess = false;
324
+
325
+ traverse(node, {
326
+ MemberExpression: (path) => {
327
+ if (this.isEnvAccess(path.node, 'typescript')) {
328
+ hasEnvAccess = true;
329
+ path.stop();
330
+ }
331
+ }
332
+ }, null, {});
333
+
334
+ return hasEnvAccess;
335
+ }
336
+
337
+ containsFailFast(node) {
338
+ // Check if node contains fail-fast patterns
339
+ let hasFailFast = false;
340
+
341
+ traverse(node, {
342
+ CallExpression: (path) => {
343
+ if (this.isFailFastCall(path.node, 'typescript')) {
344
+ hasFailFast = true;
345
+ path.stop();
346
+ }
347
+ },
348
+ ThrowStatement: () => {
349
+ hasFailFast = true;
350
+ }
351
+ }, null, {});
352
+
353
+ return hasFailFast;
354
+ }
355
+
356
+ isConfigRelatedError(node) {
357
+ // Check if the error message is configuration-related
358
+ if (t.isThrowStatement(node) && t.isNewExpression(node.argument)) {
359
+ const args = node.argument.arguments;
360
+ if (args.length > 0 && t.isStringLiteral(args[0])) {
361
+ const message = args[0].value.toLowerCase();
362
+ const configKeywords = ['config', 'environment', 'env', 'missing', 'required', 'invalid'];
363
+ return configKeywords.some(keyword => message.includes(keyword));
364
+ }
365
+ }
366
+ return false;
367
+ }
368
+ }
369
+
370
+ module.exports = C073SymbolBasedAnalyzer;
@@ -0,0 +1,152 @@
1
+ # S057 - Log with UTC Timestamps
2
+
3
+ ## Overview
4
+ Enforce UTC usage in logging and time formatting to ensure consistency across systems and avoid timezone-related issues in log analysis.
5
+
6
+ ## Rule Details
7
+
8
+ This rule enforces:
9
+ - **UTC Timestamps Only**: All logged timestamps must use UTC format
10
+ - **ISO 8601/RFC3339 Standard**: Prefer standardized time formats
11
+ - **No Local Time**: Prevent usage of local time in logs
12
+ - **Framework Configuration**: Ensure logging frameworks are configured for UTC
13
+
14
+ ## ❌ Incorrect Examples
15
+
16
+ ```javascript
17
+ // Using local time
18
+ console.log('Event at:', new Date().toString());
19
+ console.log('Timestamp:', new Date().toLocaleString());
20
+
21
+ // Non-UTC moment.js
22
+ const moment = require('moment');
23
+ logger.info(`Event time: ${moment().format()}`);
24
+
25
+ // Local DateTime patterns
26
+ logger.error(`Error at ${DateTime.now()}`);
27
+ logger.warn(`Time: ${LocalDateTime.now()}`);
28
+
29
+ // Framework without UTC config
30
+ const winston = require('winston');
31
+ const logger = winston.createLogger({
32
+ level: 'info',
33
+ format: winston.format.json(),
34
+ // Missing UTC timezone configuration
35
+ });
36
+ ```
37
+
38
+ ## ✅ Correct Examples
39
+
40
+ ```javascript
41
+ // Using UTC ISO format
42
+ console.log('Event at:', new Date().toISOString());
43
+ logger.info(`Event time: ${new Date().toISOString()}`);
44
+
45
+ // UTC moment.js
46
+ const moment = require('moment');
47
+ logger.info(`Event time: ${moment.utc().format()}`);
48
+
49
+ // UTC DateTime patterns
50
+ logger.error(`Error at ${Instant.now()}`);
51
+ logger.warn(`Time: ${OffsetDateTime.now(ZoneOffset.UTC)}`);
52
+
53
+ // Proper framework configuration
54
+ const winston = require('winston');
55
+ const logger = winston.createLogger({
56
+ level: 'info',
57
+ format: winston.format.combine(
58
+ winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
59
+ winston.format.timezone('UTC'),
60
+ winston.format.json()
61
+ )
62
+ });
63
+ ```
64
+
65
+ ## Configuration Options
66
+
67
+ ### disallowedDatePatterns
68
+ Patterns that create timezone-dependent timestamps:
69
+ - `new Date().toString()`
70
+ - `new Date().toLocaleString()`
71
+ - `DateTime.now()`
72
+ - `LocalDateTime.now()`
73
+ - `moment().format()`
74
+
75
+ ### allowedUtcPatterns
76
+ UTC-safe timestamp patterns:
77
+ - `toISOString()`
78
+ - `Instant.now()`
79
+ - `moment.utc()`
80
+ - `dayjs.utc()`
81
+ - `OffsetDateTime.now(ZoneOffset.UTC)`
82
+
83
+ ### logFrameworks
84
+ Supported logging frameworks for configuration checking:
85
+ - Winston
86
+ - Pino
87
+ - Bunyan
88
+ - Log4js
89
+ - Console methods
90
+
91
+ ## Benefits
92
+
93
+ 1. **Consistent Logs**: All timestamps in the same timezone
94
+ 2. **Global Compatibility**: Works across multiple regions/servers
95
+ 3. **Easy Analysis**: No timezone conversion needed for log correlation
96
+ 4. **Audit Compliance**: Standardized timestamps for security auditing
97
+ 5. **Debugging**: Simplified troubleshooting across distributed systems
98
+
99
+ ## Security Implications
100
+
101
+ - **Audit Trail**: Consistent timestamps critical for security incident investigation
102
+ - **Compliance**: Many security standards require UTC logging
103
+ - **Forensics**: Timezone consistency essential for timeline reconstruction
104
+ - **Correlation**: Multi-system log correlation requires synchronized time
105
+
106
+ ## Related Rules
107
+
108
+ - **C019**: Log Level Usage - Proper log severity levels
109
+ - **S056**: Sensitive Data Logging - Avoid logging sensitive information
110
+ - **S058**: SSRF Protection - Related to secure logging practices
111
+
112
+ ## Implementation Notes
113
+
114
+ ### NTP Synchronization
115
+ While this rule focuses on timestamp format, ensure your systems use NTP for time synchronization:
116
+
117
+ ```bash
118
+ # Check NTP status
119
+ timedatectl status
120
+
121
+ # Enable NTP sync
122
+ sudo timedatectl set-ntp true
123
+ ```
124
+
125
+ ### Docker Configuration
126
+ For containerized applications:
127
+
128
+ ```dockerfile
129
+ # Set timezone to UTC
130
+ ENV TZ=UTC
131
+ RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
132
+ ```
133
+
134
+ ### Database Considerations
135
+ Ensure database timestamps also use UTC:
136
+
137
+ ```sql
138
+ -- PostgreSQL
139
+ SET timezone = 'UTC';
140
+
141
+ -- MySQL
142
+ SET time_zone = '+00:00';
143
+ ```
144
+
145
+ ## False Positives
146
+
147
+ This rule may flag legitimate use cases in:
148
+ - Test files (can be exempted via configuration)
149
+ - Display/UI code (where local time is appropriate)
150
+ - Time calculation utilities (where local time is intentional)
151
+
152
+ Configure exemptions in the rule configuration as needed.