@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,457 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ class S057UtcLoggingAnalyzer {
5
+ constructor(ruleId = 'S057', verbose = false) {
6
+ this.ruleId = ruleId;
7
+ this.verbose = verbose;
8
+ this.loadConfig();
9
+ }
10
+
11
+ loadConfig() {
12
+ try {
13
+ const configPath = path.join(__dirname, 'config.json');
14
+ const configContent = fs.readFileSync(configPath, 'utf8');
15
+ const config = JSON.parse(configContent);
16
+
17
+ this.disallowedDatePatterns = config.options.disallowedDatePatterns || [];
18
+ this.allowedUtcPatterns = config.options.allowedUtcPatterns || [];
19
+ this.logFrameworks = config.options.logFrameworks || [];
20
+ this.logStatements = config.options.logStatements || [];
21
+ this.configChecks = config.options.configChecks || [];
22
+ this.policy = config.options.policy || {};
23
+ this.thresholds = config.options.thresholds || {};
24
+ this.exemptions = config.options.exemptions || {};
25
+
26
+ if (this.verbose) {
27
+ console.log(`[DEBUG] S057: Loaded config with ${this.disallowedDatePatterns.length} disallowed patterns, ${this.allowedUtcPatterns.length} allowed patterns`);
28
+ }
29
+ } catch (error) {
30
+ console.warn(`[S057] Failed to load config: ${error.message}`);
31
+ this.disallowedDatePatterns = [];
32
+ this.allowedUtcPatterns = [];
33
+ this.logFrameworks = [];
34
+ this.logStatements = [];
35
+ this.configChecks = [];
36
+ this.policy = {};
37
+ this.thresholds = {};
38
+ this.exemptions = {};
39
+ }
40
+ }
41
+
42
+ analyze(files, options = {}) {
43
+ this.verbose = options.verbose || false;
44
+ const violations = [];
45
+
46
+ if (!Array.isArray(files)) {
47
+ files = [files];
48
+ }
49
+
50
+ for (const filePath of files) {
51
+ if (this.verbose) {
52
+ console.log(`[DEBUG] 🎯 S057: Analyzing ${filePath.split('/').pop()}`);
53
+ }
54
+
55
+ try {
56
+ const content = fs.readFileSync(filePath, 'utf8');
57
+ const fileExtension = path.extname(filePath);
58
+ const fileViolations = this.analyzeFile(filePath, content, fileExtension);
59
+ violations.push(...fileViolations);
60
+ } catch (error) {
61
+ console.warn(`[S057] Error analyzing ${filePath}: ${error.message}`);
62
+ }
63
+ }
64
+
65
+ if (this.verbose) {
66
+ console.log(`[DEBUG] 🎯 S057: Found ${violations.length} UTC logging violations`);
67
+ }
68
+
69
+ return violations;
70
+ }
71
+
72
+ analyzeFile(filePath, content, fileExtension) {
73
+ const language = this.detectLanguage(fileExtension);
74
+ if (!language) {
75
+ return [];
76
+ }
77
+
78
+ // Check if file is exempted (test files)
79
+ if (this.isExemptedFile(filePath)) {
80
+ if (this.verbose) {
81
+ console.log(`[DEBUG] 🔍 S057: Skipping exempted file: ${filePath.split('/').pop()}`);
82
+ }
83
+ return [];
84
+ }
85
+
86
+ return this.analyzeWithHeuristic(filePath, content, language);
87
+ }
88
+
89
+ detectLanguage(fileExtension) {
90
+ const extensions = {
91
+ '.ts': 'typescript',
92
+ '.tsx': 'typescript',
93
+ '.js': 'javascript',
94
+ '.jsx': 'javascript',
95
+ '.mjs': 'javascript'
96
+ };
97
+ return extensions[fileExtension] || null;
98
+ }
99
+
100
+ isExemptedFile(filePath) {
101
+ if (!this.exemptions.allowedPatterns) return false;
102
+
103
+ for (const pattern of this.exemptions.allowedPatterns) {
104
+ const regex = new RegExp(pattern, 'i');
105
+ if (regex.test(filePath)) {
106
+ return true;
107
+ }
108
+ }
109
+ return false;
110
+ }
111
+
112
+ analyzeWithHeuristic(filePath, content, language) {
113
+ const violations = [];
114
+ const lines = content.split('\n');
115
+
116
+ // Find logging statements with date/time usage
117
+ const logWithTimeStatements = this.detectLogWithTimeStatements(content, lines);
118
+
119
+ if (this.verbose) {
120
+ console.log(`[DEBUG] 🔍 S057: Found ${logWithTimeStatements.length} log statements with time usage`);
121
+ }
122
+
123
+ for (const logStatement of logWithTimeStatements) {
124
+ // Check if using disallowed date patterns
125
+ const disallowedPattern = this.checkDisallowedDatePattern(logStatement);
126
+ if (disallowedPattern) {
127
+ violations.push({
128
+ ruleId: this.ruleId,
129
+ message: `Non-UTC timestamp in log: '${disallowedPattern.pattern}' should use UTC format like toISOString()`,
130
+ severity: 'warning',
131
+ line: logStatement.line,
132
+ column: logStatement.column,
133
+ filePath: filePath,
134
+ details: {
135
+ violationType: 'non_utc_timestamp',
136
+ detectedPattern: disallowedPattern.pattern,
137
+ suggestion: this.getSuggestion(disallowedPattern.pattern),
138
+ logStatement: logStatement.fullStatement
139
+ }
140
+ });
141
+ }
142
+
143
+ // Check if NOT using allowed UTC patterns when dealing with time
144
+ const hasTimeReference = this.hasTimeReference(logStatement.fullStatement);
145
+ const hasAllowedUtcPattern = this.hasAllowedUtcPattern(logStatement.fullStatement);
146
+
147
+ if (hasTimeReference && !hasAllowedUtcPattern && !disallowedPattern) {
148
+ violations.push({
149
+ ruleId: this.ruleId,
150
+ message: `Log statement with time should use UTC format (toISOString(), moment.utc(), etc.)`,
151
+ severity: 'warning',
152
+ line: logStatement.line,
153
+ column: logStatement.column,
154
+ filePath: filePath,
155
+ details: {
156
+ violationType: 'missing_utc_format',
157
+ suggestion: 'Use new Date().toISOString() or moment.utc().format() for consistent UTC timestamps',
158
+ logStatement: logStatement.fullStatement
159
+ }
160
+ });
161
+ }
162
+ }
163
+
164
+ // Check logging framework configuration
165
+ const configViolations = this.checkLoggingConfig(filePath, content, lines);
166
+ violations.push(...configViolations);
167
+
168
+ return violations;
169
+ }
170
+
171
+ detectLogWithTimeStatements(content, lines) {
172
+ const statements = [];
173
+
174
+ for (let i = 0; i < lines.length; i++) {
175
+ const line = lines[i].trim();
176
+ const lineNumber = i + 1;
177
+
178
+ // Skip comments
179
+ if (line.startsWith('//') || line.startsWith('/*') || line.startsWith('*')) {
180
+ continue;
181
+ }
182
+
183
+ // Check for log statements
184
+ for (const logPattern of this.logStatements) {
185
+ const regex = new RegExp(logPattern, 'gi');
186
+ let match;
187
+
188
+ while ((match = regex.exec(line)) !== null) {
189
+ const columnPosition = match.index + 1;
190
+
191
+ // Check if this log statement contains time-related code
192
+ if (this.containsTimeReference(line)) {
193
+ statements.push({
194
+ line: lineNumber,
195
+ column: columnPosition,
196
+ fullStatement: line,
197
+ logPattern: logPattern
198
+ });
199
+ }
200
+ }
201
+ }
202
+ }
203
+
204
+ if (this.verbose) {
205
+ console.log(`[DEBUG] 🔍 S057: Found ${statements.length} log statements with time references`);
206
+ }
207
+
208
+ return statements;
209
+ }
210
+
211
+ containsTimeReference(statement) {
212
+ const timeKeywords = [
213
+ 'new Date',
214
+ 'Date\\.',
215
+ 'moment',
216
+ 'dayjs',
217
+ 'DateTime',
218
+ 'LocalDateTime',
219
+ 'Instant',
220
+ 'ZonedDateTime',
221
+ 'Calendar',
222
+ 'timestamp',
223
+ 'time',
224
+ 'date'
225
+ ];
226
+
227
+ for (const keyword of timeKeywords) {
228
+ const regex = new RegExp(keyword, 'i');
229
+ if (regex.test(statement)) {
230
+ return true;
231
+ }
232
+ }
233
+
234
+ return false;
235
+ }
236
+
237
+ checkDisallowedDatePattern(logStatement) {
238
+ for (const pattern of this.disallowedDatePatterns) {
239
+ const regex = new RegExp(pattern, 'gi');
240
+ const match = regex.exec(logStatement.fullStatement);
241
+ if (match) {
242
+ return {
243
+ pattern: match[0],
244
+ regexPattern: pattern
245
+ };
246
+ }
247
+ }
248
+ return null;
249
+ }
250
+
251
+ hasTimeReference(statement) {
252
+ // More specific time reference check - must be actual time creation/formatting
253
+ const timeCreationPatterns = [
254
+ 'new Date\\(',
255
+ 'Date\\.',
256
+ 'moment\\(',
257
+ 'dayjs\\(',
258
+ 'DateTime\\.',
259
+ 'LocalDateTime\\.',
260
+ 'Instant\\.',
261
+ 'ZonedDateTime\\.',
262
+ 'Calendar\\.',
263
+ '\\.getTime\\(',
264
+ '\\.valueOf\\(',
265
+ '\\.toISOString\\(',
266
+ '\\.toString\\(',
267
+ '\\.toLocale'
268
+ ];
269
+
270
+ for (const pattern of timeCreationPatterns) {
271
+ const regex = new RegExp(pattern, 'i');
272
+ if (regex.test(statement)) {
273
+ return true;
274
+ }
275
+ }
276
+
277
+ return false;
278
+ }
279
+
280
+ hasAllowedUtcPattern(statement) {
281
+ for (const pattern of this.allowedUtcPatterns) {
282
+ const regex = new RegExp(pattern, 'i');
283
+ if (regex.test(statement)) {
284
+ return true;
285
+ }
286
+ }
287
+
288
+ // Additional safe patterns not in config
289
+ const additionalSafePatterns = [
290
+ 'winston\\.format\\.timezone\\(["\']UTC["\']\\)',
291
+ 'timezone\\(["\']UTC["\']\\)',
292
+ 'Date\\.now\\(\\)',
293
+ 'epoch',
294
+ 'unix'
295
+ ];
296
+
297
+ for (const pattern of additionalSafePatterns) {
298
+ const regex = new RegExp(pattern, 'i');
299
+ if (regex.test(statement)) {
300
+ return true;
301
+ }
302
+ }
303
+
304
+ return false;
305
+ }
306
+
307
+ checkLoggingConfig(filePath, content, lines) {
308
+ const violations = [];
309
+
310
+ // Check for winston/pino/bunyan configuration
311
+ const configPatterns = [
312
+ 'winston\\.createLogger',
313
+ 'new winston\\.Logger',
314
+ 'pino\\(',
315
+ 'bunyan\\.createLogger'
316
+ ];
317
+
318
+ for (let i = 0; i < lines.length; i++) {
319
+ const line = lines[i].trim();
320
+ const lineNumber = i + 1;
321
+
322
+ for (const configPattern of configPatterns) {
323
+ const regex = new RegExp(configPattern, 'i');
324
+ if (regex.test(line)) {
325
+ // Check if UTC configuration is present
326
+ const hasUtcConfig = this.checkUtcConfigAdvanced(content, i, lines, line);
327
+ if (!hasUtcConfig) {
328
+ violations.push({
329
+ ruleId: this.ruleId,
330
+ message: `Logging framework configuration should specify UTC timezone`,
331
+ severity: 'warning',
332
+ line: lineNumber,
333
+ column: 1,
334
+ filePath: filePath,
335
+ details: {
336
+ violationType: 'missing_utc_config',
337
+ configType: configPattern,
338
+ suggestion: 'Add timezone: "UTC" or similar UTC configuration to logger setup'
339
+ }
340
+ });
341
+ }
342
+ }
343
+ }
344
+ }
345
+
346
+ return violations;
347
+ }
348
+
349
+ checkUtcConfig(content, startLine, lines) {
350
+ // Look for UTC configuration in the next 15 lines and previous 5 lines
351
+ const searchStartLine = Math.max(0, startLine - 5);
352
+ const endLine = Math.min(startLine + 15, lines.length);
353
+
354
+ for (let i = searchStartLine; i < endLine; i++) {
355
+ const line = lines[i];
356
+
357
+ for (const configCheck of this.configChecks) {
358
+ const regex = new RegExp(configCheck, 'i');
359
+ if (regex.test(line)) {
360
+ return true;
361
+ }
362
+ }
363
+
364
+ // Additional UTC indicators
365
+ const utcIndicators = [
366
+ /['"]Z['"]/, // 'Z' or "Z" timezone indicator
367
+ /\+00:00/, // +00:00 timezone offset
368
+ /\.isoTime/, // pino.stdTimeFunctions.isoTime
369
+ /stdTimeFunctions\.isoTime/, // pino standard ISO time functions
370
+ /formatters.*timestamp.*isoTime/i // pino timestamp formatter
371
+ ];
372
+
373
+ for (const indicator of utcIndicators) {
374
+ if (indicator.test(line)) {
375
+ return true;
376
+ }
377
+ }
378
+ }
379
+
380
+ return false;
381
+ }
382
+
383
+ checkUtcConfigAdvanced(content, startLine, lines, currentLine) {
384
+ // First, use the basic check
385
+ if (this.checkUtcConfig(content, startLine, lines)) {
386
+ return true;
387
+ }
388
+
389
+ // Advanced: Check if pino() is called with a config variable
390
+ const pinoCallMatch = currentLine.match(/pino\(\s*(\w+)\s*\)/);
391
+ if (pinoCallMatch) {
392
+ const configVarName = pinoCallMatch[1];
393
+
394
+ // Look for the config variable definition in the entire file
395
+ for (let i = 0; i < lines.length; i++) {
396
+ const line = lines[i];
397
+
398
+ // Check if this line defines the config variable or calls a function that creates it
399
+ if (line.includes(`${configVarName} =`) || line.includes(`const ${configVarName}`) ||
400
+ line.includes(`let ${configVarName}`) || line.includes(`var ${configVarName}`)) {
401
+
402
+ // Check the function call that creates the config
403
+ const functionCallMatch = line.match(/=\s*(\w+)\s*\(/);
404
+ if (functionCallMatch) {
405
+ const functionName = functionCallMatch[1];
406
+
407
+ // Look for the function definition and check for UTC config in it
408
+ for (let j = 0; j < lines.length; j++) {
409
+ const funcLine = lines[j];
410
+ if (funcLine.includes(`const ${functionName}`) || funcLine.includes(`function ${functionName}`)) {
411
+ // Check next 50 lines for UTC configuration
412
+ for (let k = j; k < Math.min(j + 50, lines.length); k++) {
413
+ const checkLine = lines[k];
414
+
415
+ // Check for pino stdTimeFunctions.isoTime
416
+ if (/timestamp.*pino\.stdTimeFunctions\.isoTime|stdTimeFunctions\.isoTime/.test(checkLine)) {
417
+ return true;
418
+ }
419
+
420
+ // Check for other UTC indicators
421
+ for (const configCheck of this.configChecks) {
422
+ const regex = new RegExp(configCheck, 'i');
423
+ if (regex.test(checkLine)) {
424
+ return true;
425
+ }
426
+ }
427
+ }
428
+ }
429
+ }
430
+ }
431
+ }
432
+ }
433
+ }
434
+
435
+ return false;
436
+ }
437
+
438
+ getSuggestion(disallowedPattern) {
439
+ const suggestions = {
440
+ 'new Date().toString()': 'new Date().toISOString()',
441
+ 'new Date().toLocaleString()': 'new Date().toISOString()',
442
+ 'new Date().toLocaleDateString()': 'new Date().toISOString().split("T")[0]',
443
+ 'new Date().toLocaleTimeString()': 'new Date().toISOString().split("T")[1]',
444
+ 'moment().format()': 'moment.utc().format()',
445
+ 'moment()': 'moment.utc()',
446
+ 'dayjs().format()': 'dayjs.utc().format()',
447
+ 'DateTime.now()': 'Instant.now()',
448
+ 'LocalDateTime.now()': 'OffsetDateTime.now(ZoneOffset.UTC)',
449
+ '.getTime()': '.toISOString()',
450
+ '.valueOf()': '.toISOString()'
451
+ };
452
+
453
+ return suggestions[disallowedPattern] || 'Use UTC equivalent like toISOString()';
454
+ }
455
+ }
456
+
457
+ module.exports = S057UtcLoggingAnalyzer;
@@ -0,0 +1,105 @@
1
+ {
2
+ "ruleId": "S057",
3
+ "name": "Log with UTC Timestamps",
4
+ "description": "Ensure all logs use synchronized UTC time with ISO 8601/RFC3339 format to avoid timezone discrepancies across systems.",
5
+ "category": "security",
6
+ "severity": "warning",
7
+ "options": {
8
+ "disallowedDatePatterns": [
9
+ "new Date\\(\\)\\.toString\\(",
10
+ "new Date\\(\\)\\.toLocaleString\\(",
11
+ "new Date\\(\\)\\.toLocaleDateString\\(",
12
+ "new Date\\(\\)\\.toLocaleTimeString\\(",
13
+ "DateTime\\.now\\(",
14
+ "LocalDateTime\\.now\\(",
15
+ "Calendar\\.getInstance\\(",
16
+ "ZonedDateTime\\.now\\(\\s*\\)",
17
+ "moment\\(\\)\\.format\\(",
18
+ "moment\\(\\)",
19
+ "dayjs\\(\\)\\.format\\(",
20
+ "date-fns format\\(",
21
+ "\\.getTime\\(\\)",
22
+ "\\.valueOf\\(\\)"
23
+ ],
24
+ "allowedUtcPatterns": [
25
+ "toISOString\\(",
26
+ "Instant\\.now\\(",
27
+ "OffsetDateTime\\.now\\(ZoneOffset\\.UTC\\)",
28
+ "ZonedDateTime\\.now\\(ZoneId\\.of\\(\"UTC\"\\)\\)",
29
+ "DateTimeFormatter\\.ISO_INSTANT",
30
+ "DateTimeFormatter\\.RFC_1123_DATE_TIME",
31
+ "moment\\.utc\\(",
32
+ "dayjs\\.utc\\(",
33
+ "new Date\\(\\)\\.toISOString\\(",
34
+ "new Date\\(\\)\\.getUTCFullYear\\(",
35
+ "new Date\\(\\)\\.getUTCMonth\\(",
36
+ "new Date\\(\\)\\.getUTCDate\\(",
37
+ "Date\\.now\\("
38
+ ],
39
+ "logFrameworks": [
40
+ "winston",
41
+ "pino",
42
+ "bunyan",
43
+ "log4js",
44
+ "log4j",
45
+ "slf4j",
46
+ "logback",
47
+ "console\\.log",
48
+ "console\\.info",
49
+ "console\\.warn",
50
+ "console\\.error",
51
+ "logger\\.",
52
+ "log\\."
53
+ ],
54
+ "logStatements": [
55
+ "console\\.(?:log|info|warn|error|debug)",
56
+ "logger\\.(?:log|info|warn|error|debug|trace)",
57
+ "log\\.(?:log|info|warn|error|debug|trace)",
58
+ "winston\\.",
59
+ "pino\\.",
60
+ "bunyan\\."
61
+ ],
62
+ "requiredConfig": {
63
+ "timezone": "UTC",
64
+ "format": ["ISO8601", "RFC3339", "ISO_INSTANT"],
65
+ "ntpSync": true
66
+ },
67
+ "configChecks": [
68
+ "timezone.*UTC",
69
+ "tz.*UTC",
70
+ "timeZone.*UTC",
71
+ "utc.*true",
72
+ "ISO8601",
73
+ "RFC3339",
74
+ "ISO_INSTANT",
75
+ "'Z'",
76
+ "\"Z\"",
77
+ "\\+00:00",
78
+ "Z'$",
79
+ "\\.l'Z'",
80
+ "HH:mm:ss'Z'",
81
+ "timestamp.*isoTime"
82
+ ],
83
+ "policy": {
84
+ "requireUtcFormat": true,
85
+ "requireNtpSync": false,
86
+ "blockLocalTime": true,
87
+ "enforceIsoFormat": true
88
+ },
89
+ "thresholds": {
90
+ "maxNonUtcLogs": 0,
91
+ "maxInconsistentFormats": 1
92
+ },
93
+ "exemptions": {
94
+ "allowedInTests": false,
95
+ "allowedInDev": false,
96
+ "allowedPatterns": [
97
+ "\\.test\\.(?:js|ts)$",
98
+ "\\.spec\\.(?:js|ts)$",
99
+ "/test/.*\\.(?:js|ts)$",
100
+ "/tests/.*\\.(?:js|ts)$",
101
+ "/__tests__/.*\\.(?:js|ts)$"
102
+ ]
103
+ }
104
+ }
105
+ }