@sun-asterisk/sunlint 1.3.6 → 1.3.8
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 +76 -1
- package/config/defaults/default.json +2 -1
- package/config/rule-analysis-strategies.js +20 -0
- package/config/rules/enhanced-rules-registry.json +230 -43
- package/core/analysis-orchestrator.js +9 -5
- package/core/file-targeting-service.js +83 -7
- package/core/performance-optimizer.js +8 -2
- package/package.json +1 -1
- package/rules/common/C065_one_behavior_per_test/analyzer.js +851 -0
- package/rules/common/C065_one_behavior_per_test/config.json +95 -0
- package/rules/common/C073_validate_required_config_on_startup/README.md +110 -0
- package/rules/common/C073_validate_required_config_on_startup/analyzer.js +770 -0
- package/rules/common/C073_validate_required_config_on_startup/config.json +46 -0
- package/rules/common/C073_validate_required_config_on_startup/symbol-based-analyzer.js +370 -0
- package/rules/security/S037_cache_headers/README.md +128 -0
- package/rules/security/S037_cache_headers/analyzer.js +263 -0
- package/rules/security/S037_cache_headers/config.json +50 -0
- package/rules/security/S037_cache_headers/regex-based-analyzer.js +463 -0
- package/rules/security/S037_cache_headers/symbol-based-analyzer.js +546 -0
- package/rules/security/S038_no_version_headers/README.md +234 -0
- package/rules/security/S038_no_version_headers/analyzer.js +262 -0
- package/rules/security/S038_no_version_headers/config.json +49 -0
- package/rules/security/S038_no_version_headers/regex-based-analyzer.js +339 -0
- package/rules/security/S038_no_version_headers/symbol-based-analyzer.js +375 -0
- package/rules/security/S039_no_session_tokens_in_url/README.md +198 -0
- package/rules/security/S039_no_session_tokens_in_url/analyzer.js +262 -0
- package/rules/security/S039_no_session_tokens_in_url/config.json +92 -0
- package/rules/security/S039_no_session_tokens_in_url/regex-based-analyzer.js +337 -0
- package/rules/security/S039_no_session_tokens_in_url/symbol-based-analyzer.js +436 -0
- package/rules/security/S049_short_validity_tokens/analyzer.js +175 -0
- package/rules/security/S049_short_validity_tokens/config.json +124 -0
- package/rules/security/S049_short_validity_tokens/regex-based-analyzer.js +295 -0
- package/rules/security/S049_short_validity_tokens/symbol-based-analyzer.js +389 -0
- package/rules/security/S051_password_length_policy/analyzer.js +410 -0
- package/rules/security/S051_password_length_policy/config.json +83 -0
- package/rules/security/S052_weak_otp_entropy/analyzer.js +403 -0
- package/rules/security/S052_weak_otp_entropy/config.json +57 -0
- package/rules/security/S054_no_default_accounts/README.md +129 -0
- package/rules/security/S054_no_default_accounts/analyzer.js +792 -0
- package/rules/security/S054_no_default_accounts/config.json +101 -0
- package/rules/security/S056_log_injection_protection/analyzer.js +242 -0
- package/rules/security/S056_log_injection_protection/config.json +148 -0
- package/rules/security/S056_log_injection_protection/regex-based-analyzer.js +120 -0
- package/rules/security/S056_log_injection_protection/symbol-based-analyzer.js +287 -0
- package/rules/security/S057_utc_logging/README.md +152 -0
- package/rules/security/S057_utc_logging/analyzer.js +457 -0
- package/rules/security/S057_utc_logging/config.json +105 -0
- package/rules/security/S058_no_ssrf/README.md +180 -0
- package/rules/security/S058_no_ssrf/analyzer.js +403 -0
- package/rules/security/S058_no_ssrf/config.json +125 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S056 Symbol-Based Analyzer - Protect against Log Injection attacks
|
|
3
|
+
* Uses TypeScript compiler API for semantic analysis
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const ts = require("typescript");
|
|
7
|
+
|
|
8
|
+
class S056SymbolBasedAnalyzer {
|
|
9
|
+
constructor(semanticEngine = null) {
|
|
10
|
+
this.semanticEngine = semanticEngine;
|
|
11
|
+
this.ruleId = "S056";
|
|
12
|
+
this.category = "security";
|
|
13
|
+
|
|
14
|
+
// Log method names that can be vulnerable
|
|
15
|
+
this.logMethods = [
|
|
16
|
+
"log",
|
|
17
|
+
"info",
|
|
18
|
+
"warn",
|
|
19
|
+
"error",
|
|
20
|
+
"debug",
|
|
21
|
+
"trace",
|
|
22
|
+
"write",
|
|
23
|
+
"writeSync"
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
// User input sources that could lead to injection
|
|
27
|
+
this.userInputSources = [
|
|
28
|
+
"req",
|
|
29
|
+
"request",
|
|
30
|
+
"params",
|
|
31
|
+
"query",
|
|
32
|
+
"body",
|
|
33
|
+
"headers",
|
|
34
|
+
"cookies",
|
|
35
|
+
"session"
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
// Dangerous characters for log injection
|
|
39
|
+
this.dangerousCharacters = [
|
|
40
|
+
"\\r",
|
|
41
|
+
"\\n",
|
|
42
|
+
"\\r\\n",
|
|
43
|
+
"\\u000a",
|
|
44
|
+
"\\u000d",
|
|
45
|
+
"%0a",
|
|
46
|
+
"%0d",
|
|
47
|
+
"\\x0a",
|
|
48
|
+
"\\x0d"
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
// Secure log patterns
|
|
52
|
+
this.securePatterns = [
|
|
53
|
+
"sanitize",
|
|
54
|
+
"escape",
|
|
55
|
+
"clean",
|
|
56
|
+
"filter",
|
|
57
|
+
"validate",
|
|
58
|
+
"replace",
|
|
59
|
+
"strip"
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Initialize analyzer with semantic engine
|
|
65
|
+
*/
|
|
66
|
+
async initialize(semanticEngine) {
|
|
67
|
+
this.semanticEngine = semanticEngine;
|
|
68
|
+
if (this.verbose) {
|
|
69
|
+
console.log(`🔍 [${this.ruleId}] Symbol: Semantic engine initialized`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async analyze(filePath) {
|
|
74
|
+
if (this.verbose) {
|
|
75
|
+
console.log(
|
|
76
|
+
`🔍 [${this.ruleId}] Symbol: Starting analysis for ${filePath}`
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!this.semanticEngine) {
|
|
81
|
+
if (this.verbose) {
|
|
82
|
+
console.log(
|
|
83
|
+
`🔍 [${this.ruleId}] Symbol: No semantic engine available, skipping`
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const sourceFile = this.semanticEngine.getSourceFile(filePath);
|
|
91
|
+
if (!sourceFile) {
|
|
92
|
+
if (this.verbose) {
|
|
93
|
+
console.log(
|
|
94
|
+
`🔍 [${this.ruleId}] Symbol: No source file found, trying ts-morph fallback`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
return await this.analyzeTsMorph(filePath);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (this.verbose) {
|
|
101
|
+
console.log(`🔧 [${this.ruleId}] Source file found, analyzing...`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const violations = [];
|
|
105
|
+
const typeChecker = this.semanticEngine.program?.getTypeChecker();
|
|
106
|
+
|
|
107
|
+
// Visit all nodes in the source file
|
|
108
|
+
const visit = (node) => {
|
|
109
|
+
// Check for log method calls
|
|
110
|
+
if (ts.isCallExpression(node)) {
|
|
111
|
+
this.checkLogMethodCall(node, violations, sourceFile, typeChecker);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
ts.forEachChild(node, visit);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
visit(sourceFile);
|
|
118
|
+
|
|
119
|
+
if (this.verbose) {
|
|
120
|
+
console.log(
|
|
121
|
+
`🔧 [${this.ruleId}] Symbol analysis completed: ${violations.length} violations found`
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return violations;
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.warn(`⚠ [${this.ruleId}] Symbol analysis failed:`, error.message);
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
checkLogMethodCall(node, violations, sourceFile, typeChecker) {
|
|
133
|
+
// Check if this is a logging method call
|
|
134
|
+
const methodName = this.getMethodName(node);
|
|
135
|
+
if (!methodName || !this.logMethods.includes(methodName)) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Check arguments for user input
|
|
140
|
+
if (node.arguments && node.arguments.length > 0) {
|
|
141
|
+
for (const arg of node.arguments) {
|
|
142
|
+
if (this.containsUserInput(arg, sourceFile)) {
|
|
143
|
+
const position = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
144
|
+
violations.push({
|
|
145
|
+
ruleId: this.ruleId,
|
|
146
|
+
message: `Log injection vulnerability: User input directly used in ${methodName}() call without sanitization`,
|
|
147
|
+
line: position.line + 1,
|
|
148
|
+
column: position.character + 1,
|
|
149
|
+
severity: "error",
|
|
150
|
+
category: this.category,
|
|
151
|
+
code: sourceFile.getFullText().slice(node.getStart(), node.getEnd())
|
|
152
|
+
});
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
getMethodName(callExpression) {
|
|
160
|
+
const expression = callExpression.expression;
|
|
161
|
+
|
|
162
|
+
if (ts.isIdentifier(expression)) {
|
|
163
|
+
return expression.text;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (ts.isPropertyAccessExpression(expression)) {
|
|
167
|
+
return expression.name.text;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
containsUserInput(node, sourceFile) {
|
|
174
|
+
// Check for direct user input references
|
|
175
|
+
if (ts.isIdentifier(node)) {
|
|
176
|
+
return this.userInputSources.includes(node.text);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Check for property access on user input (e.g., req.body, req.query)
|
|
180
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
181
|
+
const objectName = this.getObjectName(node);
|
|
182
|
+
return this.userInputSources.includes(objectName);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Check for element access on user input (e.g., req["body"], headers['user-agent'])
|
|
186
|
+
if (ts.isElementAccessExpression(node)) {
|
|
187
|
+
const objectName = this.getObjectName(node);
|
|
188
|
+
return this.userInputSources.includes(objectName);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Check for binary expressions (concatenation)
|
|
192
|
+
if (ts.isBinaryExpression(node)) {
|
|
193
|
+
return this.containsUserInput(node.left, sourceFile) ||
|
|
194
|
+
this.containsUserInput(node.right, sourceFile);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Check for template literals
|
|
198
|
+
if (ts.isTemplateExpression(node)) {
|
|
199
|
+
return node.templateSpans.some(span =>
|
|
200
|
+
this.containsUserInput(span.expression, sourceFile)
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Check for function calls that might return user input
|
|
205
|
+
if (ts.isCallExpression(node)) {
|
|
206
|
+
// Check if it's JSON.stringify with user input
|
|
207
|
+
const methodName = this.getMethodName(node);
|
|
208
|
+
if (methodName === "stringify" && node.arguments.length > 0) {
|
|
209
|
+
return this.containsUserInput(node.arguments[0], sourceFile);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
getObjectName(node) {
|
|
217
|
+
if (ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)) {
|
|
218
|
+
if (ts.isIdentifier(node.expression)) {
|
|
219
|
+
return node.expression.text;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Fallback analysis using ts-morph when semantic engine is not available
|
|
227
|
+
*/
|
|
228
|
+
async analyzeTsMorph(filePath) {
|
|
229
|
+
try {
|
|
230
|
+
const fs = require("fs");
|
|
231
|
+
const { Project } = require("ts-morph");
|
|
232
|
+
|
|
233
|
+
const project = new Project();
|
|
234
|
+
const sourceFile = project.addSourceFileAtPath(filePath);
|
|
235
|
+
const violations = [];
|
|
236
|
+
|
|
237
|
+
// Find all call expressions
|
|
238
|
+
sourceFile.forEachDescendant((node) => {
|
|
239
|
+
if (node.getKind() === ts.SyntaxKind.CallExpression) {
|
|
240
|
+
const callExpr = node;
|
|
241
|
+
const methodName = this.extractMethodName(callExpr.getText());
|
|
242
|
+
|
|
243
|
+
if (this.logMethods.includes(methodName)) {
|
|
244
|
+
const args = callExpr.getArguments();
|
|
245
|
+
for (const arg of args) {
|
|
246
|
+
if (this.containsUserInputText(arg.getText())) {
|
|
247
|
+
const line = sourceFile.getLineAndColumnAtPos(node.getStart()).line;
|
|
248
|
+
const column = sourceFile.getLineAndColumnAtPos(node.getStart()).column;
|
|
249
|
+
|
|
250
|
+
violations.push({
|
|
251
|
+
ruleId: this.ruleId,
|
|
252
|
+
message: `Log injection vulnerability: User input directly used in ${methodName}() call without sanitization`,
|
|
253
|
+
line: line,
|
|
254
|
+
column: column,
|
|
255
|
+
severity: "error",
|
|
256
|
+
category: this.category,
|
|
257
|
+
code: node.getText()
|
|
258
|
+
});
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
return violations;
|
|
267
|
+
} catch (error) {
|
|
268
|
+
console.warn(`⚠ [${this.ruleId}] ts-morph analysis failed:`, error.message);
|
|
269
|
+
return [];
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
extractMethodName(callText) {
|
|
274
|
+
const match = callText.match(/(\w+)\s*\(/);
|
|
275
|
+
return match ? match[1] : null;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
containsUserInputText(text) {
|
|
279
|
+
return this.userInputSources.some(source => text.includes(source));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
cleanup() {
|
|
283
|
+
// Cleanup resources if needed
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
module.exports = S056SymbolBasedAnalyzer;
|
|
@@ -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.
|