@sun-asterisk/sunlint 1.3.7 → 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.
Files changed (38) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/config/defaults/default.json +2 -1
  3. package/config/rule-analysis-strategies.js +20 -0
  4. package/config/rules/enhanced-rules-registry.json +190 -35
  5. package/core/file-targeting-service.js +83 -7
  6. package/package.json +1 -1
  7. package/rules/common/C065_one_behavior_per_test/analyzer.js +851 -0
  8. package/rules/common/C065_one_behavior_per_test/config.json +95 -0
  9. package/rules/security/S037_cache_headers/README.md +128 -0
  10. package/rules/security/S037_cache_headers/analyzer.js +263 -0
  11. package/rules/security/S037_cache_headers/config.json +50 -0
  12. package/rules/security/S037_cache_headers/regex-based-analyzer.js +463 -0
  13. package/rules/security/S037_cache_headers/symbol-based-analyzer.js +546 -0
  14. package/rules/security/S038_no_version_headers/README.md +234 -0
  15. package/rules/security/S038_no_version_headers/analyzer.js +262 -0
  16. package/rules/security/S038_no_version_headers/config.json +49 -0
  17. package/rules/security/S038_no_version_headers/regex-based-analyzer.js +339 -0
  18. package/rules/security/S038_no_version_headers/symbol-based-analyzer.js +375 -0
  19. package/rules/security/S039_no_session_tokens_in_url/README.md +198 -0
  20. package/rules/security/S039_no_session_tokens_in_url/analyzer.js +262 -0
  21. package/rules/security/S039_no_session_tokens_in_url/config.json +92 -0
  22. package/rules/security/S039_no_session_tokens_in_url/regex-based-analyzer.js +337 -0
  23. package/rules/security/S039_no_session_tokens_in_url/symbol-based-analyzer.js +436 -0
  24. package/rules/security/S049_short_validity_tokens/analyzer.js +175 -0
  25. package/rules/security/S049_short_validity_tokens/config.json +124 -0
  26. package/rules/security/S049_short_validity_tokens/regex-based-analyzer.js +295 -0
  27. package/rules/security/S049_short_validity_tokens/symbol-based-analyzer.js +389 -0
  28. package/rules/security/S051_password_length_policy/analyzer.js +410 -0
  29. package/rules/security/S051_password_length_policy/config.json +83 -0
  30. package/rules/security/S052_weak_otp_entropy/analyzer.js +403 -0
  31. package/rules/security/S052_weak_otp_entropy/config.json +57 -0
  32. package/rules/security/S054_no_default_accounts/README.md +129 -0
  33. package/rules/security/S054_no_default_accounts/analyzer.js +792 -0
  34. package/rules/security/S054_no_default_accounts/config.json +101 -0
  35. package/rules/security/S056_log_injection_protection/analyzer.js +242 -0
  36. package/rules/security/S056_log_injection_protection/config.json +148 -0
  37. package/rules/security/S056_log_injection_protection/regex-based-analyzer.js +120 -0
  38. package/rules/security/S056_log_injection_protection/symbol-based-analyzer.js +287 -0
@@ -0,0 +1,436 @@
1
+ /**
2
+ * S039 Symbol-Based Analyzer - Do not pass Session Tokens via URL parameters
3
+ * Enhanced to analyze per route handler for URL parameter token exposure
4
+ */
5
+
6
+ class S039SymbolBasedAnalyzer {
7
+ constructor(semanticEngine) {
8
+ this.ruleId = "S039";
9
+ this.semanticEngine = semanticEngine;
10
+
11
+ // Session token parameter names to detect
12
+ this.sessionTokenParams = [
13
+ "sessionId",
14
+ "session_id",
15
+ "session-id",
16
+ "sessionToken",
17
+ "session_token",
18
+ "session-token",
19
+ "authToken",
20
+ "auth_token",
21
+ "auth-token",
22
+ "authorization",
23
+ "bearer",
24
+ "jwt",
25
+ "jwtToken",
26
+ "jwt_token",
27
+ "jwt-token",
28
+ "accessToken",
29
+ "access_token",
30
+ "access-token",
31
+ "refreshToken",
32
+ "refresh_token",
33
+ "refresh-token",
34
+ "apiKey",
35
+ "api_key",
36
+ "api-key",
37
+ "csrfToken",
38
+ "csrf_token",
39
+ "csrf-token",
40
+ "xsrfToken",
41
+ "xsrf_token",
42
+ "xsrf-token",
43
+ "token",
44
+ "apiToken",
45
+ "api_token",
46
+ "api-token",
47
+ "sid",
48
+ "sessionkey",
49
+ "session_key",
50
+ "session-key",
51
+ "userToken",
52
+ "user_token",
53
+ "user-token",
54
+ "authKey",
55
+ "auth_key",
56
+ "auth-key",
57
+ "securityToken",
58
+ "security_token",
59
+ "security-token",
60
+ ];
61
+
62
+ // Pattern to detect session token-like values
63
+ this.tokenValuePattern = /^[a-zA-Z0-9+/=\-_.]{16,}$/;
64
+ }
65
+
66
+ async initialize() {}
67
+
68
+ analyze(sourceFile, filePath) {
69
+ const violations = [];
70
+
71
+ // Skip files that are unlikely to be route handlers
72
+ const skipPatterns = [
73
+ /\.dto\.ts$/,
74
+ /\.interface\.ts$/,
75
+ /\.module\.ts$/,
76
+ /\.service\.spec\.ts$/,
77
+ /\.controller\.spec\.ts$/,
78
+ /\.spec\.ts$/,
79
+ /\.test\.ts$/,
80
+ /\.d\.ts$/,
81
+ /\.types\.ts$/,
82
+ /\.constants?.ts$/,
83
+ /\.config\.ts$/,
84
+ ];
85
+
86
+ const shouldSkip = skipPatterns.some((pattern) => pattern.test(filePath));
87
+ if (shouldSkip) {
88
+ return violations;
89
+ }
90
+
91
+ try {
92
+ const { SyntaxKind } = require("ts-morph");
93
+
94
+ // Find all function expressions and arrow functions that could be route handlers
95
+ const routeHandlers = [];
96
+
97
+ // Express route patterns: app.get("/path", (req, res) => {...})
98
+ const callExpressions = sourceFile.getDescendantsOfKind(
99
+ SyntaxKind.CallExpression
100
+ );
101
+
102
+ for (const call of callExpressions) {
103
+ const expression = call.getExpression();
104
+ if (expression.getKind() === SyntaxKind.PropertyAccessExpression) {
105
+ const methodName = expression.getNameNode().getText();
106
+ if (/^(get|post|put|delete|patch|all|use)$/.test(methodName)) {
107
+ const args = call.getArguments();
108
+ const lastArg = args[args.length - 1];
109
+ // The last argument should be the handler function
110
+ if (
111
+ lastArg.getKind() === SyntaxKind.ArrowFunction ||
112
+ lastArg.getKind() === SyntaxKind.FunctionExpression
113
+ ) {
114
+ routeHandlers.push({
115
+ handler: lastArg,
116
+ routeCall: call,
117
+ type: "express",
118
+ });
119
+ }
120
+ }
121
+ }
122
+ }
123
+
124
+ // Next.js export functions
125
+ const exportAssignments = sourceFile.getDescendantsOfKind(
126
+ SyntaxKind.ExportAssignment
127
+ );
128
+ const exportDeclarations = sourceFile.getDescendantsOfKind(
129
+ SyntaxKind.ExportDeclaration
130
+ );
131
+ const functionDeclarations = sourceFile.getDescendantsOfKind(
132
+ SyntaxKind.FunctionDeclaration
133
+ );
134
+
135
+ for (const func of functionDeclarations) {
136
+ const name = func.getName();
137
+ if (name && /^(GET|POST|PUT|DELETE|PATCH|handler)$/.test(name)) {
138
+ routeHandlers.push({
139
+ handler: func,
140
+ type: "nextjs",
141
+ });
142
+ }
143
+ }
144
+
145
+ // NestJS Controller methods with decorators
146
+ const methodDeclarations = sourceFile.getDescendantsOfKind(
147
+ SyntaxKind.MethodDeclaration
148
+ );
149
+
150
+ for (const method of methodDeclarations) {
151
+ const decorators = method.getDecorators();
152
+ const hasRouteDecorator = decorators.some((decorator) => {
153
+ const decoratorName = decorator.getName();
154
+ return /^(Get|Post|Put|Delete|Patch|All)$/.test(decoratorName);
155
+ });
156
+
157
+ if (hasRouteDecorator) {
158
+ routeHandlers.push({
159
+ handler: method,
160
+ type: "nestjs",
161
+ });
162
+ }
163
+ }
164
+
165
+ // Nuxt.js defineEventHandler patterns
166
+ for (const call of callExpressions) {
167
+ const expression = call.getExpression();
168
+ if (expression.getKind() === SyntaxKind.Identifier) {
169
+ const identifier = expression.asKindOrThrow(SyntaxKind.Identifier);
170
+ if (identifier.getText() === "defineEventHandler") {
171
+ // Find the arrow function or function parameter
172
+ const args = call.getArguments();
173
+ if (args.length > 0) {
174
+ const firstArg = args[0];
175
+ if (
176
+ firstArg.getKind() === SyntaxKind.ArrowFunction ||
177
+ firstArg.getKind() === SyntaxKind.FunctionExpression
178
+ ) {
179
+ routeHandlers.push({
180
+ handler: firstArg,
181
+ type: "nuxtjs",
182
+ });
183
+ }
184
+ }
185
+ }
186
+ }
187
+ }
188
+
189
+ // Analyze each route handler for session token exposure in URL parameters
190
+ for (const { handler, routeCall, type } of routeHandlers) {
191
+ try {
192
+ const handlerViolations = this.analyzeRouteHandler(
193
+ handler,
194
+ routeCall,
195
+ type,
196
+ filePath
197
+ );
198
+ violations.push(...handlerViolations);
199
+ } catch (error) {
200
+ console.warn(
201
+ `⚠ [S039] Handler analysis failed for ${type}:`,
202
+ error.message
203
+ );
204
+ }
205
+ }
206
+ } catch (error) {
207
+ console.warn(
208
+ `⚠ [S039] Symbol analysis failed for ${filePath}:`,
209
+ error.message
210
+ );
211
+ }
212
+
213
+ return violations;
214
+ }
215
+
216
+ analyzeRouteHandler(handler, routeCall, type, filePath) {
217
+ const violations = [];
218
+
219
+ try {
220
+ const { SyntaxKind } = require("ts-morph");
221
+
222
+ // Find URL parameter access patterns within this handler
223
+ const tokenExposures = this.findTokenParametersInNode(handler);
224
+
225
+ // Report violations for exposed session tokens in URL parameters
226
+ for (const exposure of tokenExposures) {
227
+ const startLine = exposure.node.getStartLineNumber();
228
+ violations.push({
229
+ ruleId: this.ruleId,
230
+ message: `Session token '${exposure.paramName}' passed via URL parameter - use secure headers or request body instead`,
231
+ severity: "warning",
232
+ line: startLine,
233
+ column: 1,
234
+ });
235
+ }
236
+ } catch (error) {
237
+ console.warn(`⚠ [S039] Route handler analysis failed:`, error.message);
238
+ }
239
+
240
+ return violations;
241
+ }
242
+
243
+ findTokenParametersInNode(node) {
244
+ const exposures = [];
245
+
246
+ try {
247
+ const { SyntaxKind } = require("ts-morph");
248
+
249
+ // For NestJS, check decorator parameters first
250
+ if (node.getKind() === SyntaxKind.MethodDeclaration) {
251
+ const parameters = node.getParameters();
252
+ for (const param of parameters) {
253
+ const decorators = param.getDecorators();
254
+ for (const decorator of decorators) {
255
+ const decoratorName = decorator.getName();
256
+ if (decoratorName === "Query" || decoratorName === "Param") {
257
+ const args = decorator.getArguments();
258
+ if (args.length > 0) {
259
+ const firstArg = args[0];
260
+ if (firstArg.getKind() === SyntaxKind.StringLiteral) {
261
+ const paramName = firstArg.getLiteralValue();
262
+ if (this.isSessionTokenParam(paramName)) {
263
+ exposures.push({
264
+ node: param,
265
+ paramName: paramName,
266
+ accessType: decoratorName.toLowerCase(),
267
+ });
268
+ }
269
+ }
270
+ }
271
+ }
272
+ }
273
+ }
274
+ }
275
+
276
+ // Find all property access expressions for URL parameters
277
+ const propertyAccesses = node.getDescendantsOfKind(
278
+ SyntaxKind.PropertyAccessExpression
279
+ );
280
+
281
+ for (const propAccess of propertyAccesses) {
282
+ const expression = propAccess.getExpression();
283
+ const property = propAccess.getName();
284
+
285
+ // Check for req.query.paramName patterns
286
+ if (expression.getKind() === SyntaxKind.PropertyAccessExpression) {
287
+ const parentExpression = expression.getExpression();
288
+ const parentProperty = expression.getName();
289
+
290
+ // req.query.sessionToken, req.params.authToken, etc.
291
+ if (
292
+ parentProperty === "query" ||
293
+ parentProperty === "params" ||
294
+ parentProperty === "searchParams"
295
+ ) {
296
+ if (this.isSessionTokenParam(property)) {
297
+ exposures.push({
298
+ node: propAccess,
299
+ paramName: property,
300
+ accessType: parentProperty,
301
+ });
302
+ }
303
+ }
304
+ }
305
+ }
306
+
307
+ // Check for bracket notation access: req.query["access-token"]
308
+ const elementAccessExpressions = node.getDescendantsOfKind(
309
+ SyntaxKind.ElementAccessExpression
310
+ );
311
+
312
+ for (const elemAccess of elementAccessExpressions) {
313
+ const expression = elemAccess.getExpression();
314
+ const argumentExpression = elemAccess.getArgumentExpression();
315
+
316
+ if (
317
+ expression.getKind() === SyntaxKind.PropertyAccessExpression &&
318
+ argumentExpression &&
319
+ argumentExpression.getKind() === SyntaxKind.StringLiteral
320
+ ) {
321
+ const parentProperty = expression.getName();
322
+ const paramName = argumentExpression.getLiteralValue();
323
+
324
+ // req.query["sessionToken"], req.params["authToken"], etc.
325
+ if (
326
+ (parentProperty === "query" ||
327
+ parentProperty === "params" ||
328
+ parentProperty === "searchParams") &&
329
+ this.isSessionTokenParam(paramName)
330
+ ) {
331
+ exposures.push({
332
+ node: elemAccess,
333
+ paramName: paramName,
334
+ accessType: parentProperty,
335
+ });
336
+ }
337
+ }
338
+ }
339
+
340
+ // Check for URL.searchParams.get() patterns
341
+ const callExpressions = node.getDescendantsOfKind(
342
+ SyntaxKind.CallExpression
343
+ );
344
+
345
+ for (const call of callExpressions) {
346
+ const callExpression = call.getExpression();
347
+ if (callExpression.getKind() === SyntaxKind.PropertyAccessExpression) {
348
+ const methodName = callExpression.getName();
349
+ const objectExpression = callExpression.getExpression();
350
+
351
+ // searchParams.get("sessionToken"), URLSearchParams.get("token")
352
+ if (
353
+ methodName === "get" &&
354
+ objectExpression.getText().includes("searchParams")
355
+ ) {
356
+ const args = call.getArguments();
357
+ if (args.length > 0) {
358
+ const firstArg = args[0];
359
+ if (firstArg.getKind() === SyntaxKind.StringLiteral) {
360
+ const paramName = firstArg.getLiteralValue();
361
+ if (this.isSessionTokenParam(paramName)) {
362
+ exposures.push({
363
+ node: call,
364
+ paramName: paramName,
365
+ accessType: "searchParams",
366
+ });
367
+ }
368
+ }
369
+ }
370
+ }
371
+ }
372
+ }
373
+
374
+ // Check for object destructuring patterns
375
+ const variableDeclarations = node.getDescendantsOfKind(
376
+ SyntaxKind.VariableDeclaration
377
+ );
378
+
379
+ for (const varDecl of variableDeclarations) {
380
+ const nameNode = varDecl.getNameNode();
381
+ if (nameNode.getKind() === SyntaxKind.ObjectBindingPattern) {
382
+ const bindingPattern = nameNode.asKindOrThrow(
383
+ SyntaxKind.ObjectBindingPattern
384
+ );
385
+ const elements = bindingPattern.getElements();
386
+
387
+ const initializer = varDecl.getInitializer();
388
+ if (
389
+ initializer &&
390
+ (initializer.getText().includes("req.query") ||
391
+ initializer.getText().includes("req.params") ||
392
+ initializer.getText().includes("searchParams"))
393
+ ) {
394
+ for (const element of elements) {
395
+ let paramName = null;
396
+
397
+ // Handle both { paramName } and { "param-name": alias } patterns
398
+ const propNameNode = element.getPropertyNameNode();
399
+ const nameNode = element.getNameNode();
400
+
401
+ if (propNameNode) {
402
+ // { "param-name": alias } or { paramName: alias }
403
+ paramName = propNameNode.getText().replace(/['"]/g, "");
404
+ } else if (nameNode) {
405
+ // { paramName } shorthand
406
+ paramName = nameNode.getText();
407
+ }
408
+
409
+ if (this.isSessionTokenParam(paramName)) {
410
+ exposures.push({
411
+ node: element,
412
+ paramName: paramName,
413
+ accessType: "destructuring",
414
+ });
415
+ }
416
+ }
417
+ }
418
+ }
419
+ }
420
+ } catch (error) {
421
+ console.warn(`⚠ [S039] Parameter analysis failed:`, error.message);
422
+ }
423
+
424
+ return exposures;
425
+ }
426
+
427
+ isSessionTokenParam(paramName) {
428
+ return this.sessionTokenParams.some(
429
+ (tokenParam) => tokenParam.toLowerCase() === paramName.toLowerCase()
430
+ );
431
+ }
432
+
433
+ cleanup() {}
434
+ }
435
+
436
+ module.exports = S039SymbolBasedAnalyzer;
@@ -0,0 +1,175 @@
1
+ /**
2
+ * S049 Main Analyzer - Authentication tokens should have short validity periods
3
+ * Primary: Symbol-based analysis (when available)
4
+ * Fallback: Regex-based for all other cases
5
+ * Command: node cli.js --rule=S049 --input=examples/rule-test-fixtures/rules/S049_short_validity_tokens --engine=heuristic
6
+ */
7
+
8
+ const S049SymbolBasedAnalyzer = require("./symbol-based-analyzer.js");
9
+ const S049RegexBasedAnalyzer = require("./regex-based-analyzer.js");
10
+
11
+ class S049Analyzer {
12
+ constructor(options = {}) {
13
+ if (process.env.SUNLINT_DEBUG) {
14
+ console.log(`🔧 [S049] Constructor called with options:`, !!options);
15
+ console.log(
16
+ `🔧 [S049] Options type:`,
17
+ typeof options,
18
+ Object.keys(options || {})
19
+ );
20
+ }
21
+
22
+ this.ruleId = "S049";
23
+ this.ruleName = "Authentication tokens should have short validity periods";
24
+ this.description =
25
+ "Authentication tokens (JWT, session tokens, etc.) should have appropriately short validity periods to minimize the risk of token compromise. Long-lived tokens increase the attack surface and potential impact of token theft.";
26
+ this.semanticEngine = options.semanticEngine || null;
27
+ this.verbose = options.verbose || false;
28
+
29
+ // Configuration
30
+ this.config = {
31
+ useSymbolBased: true, // Primary approach
32
+ fallbackToRegex: true, // Secondary approach
33
+ regexBasedOnly: false, // Can be set to true for pure mode
34
+ };
35
+
36
+ // Initialize analyzers
37
+ try {
38
+ this.symbolAnalyzer = new S049SymbolBasedAnalyzer(this.semanticEngine);
39
+ if (process.env.SUNLINT_DEBUG) {
40
+ console.log(`🔧 [S049] Symbol analyzer created successfully`);
41
+ }
42
+ } catch (error) {
43
+ console.error(`🔧 [S049] Error creating symbol analyzer:`, error);
44
+ }
45
+
46
+ try {
47
+ this.regexAnalyzer = new S049RegexBasedAnalyzer(this.semanticEngine);
48
+ if (process.env.SUNLINT_DEBUG) {
49
+ console.log(`🔧 [S049] Regex analyzer created successfully`);
50
+ }
51
+ } catch (error) {
52
+ console.error(`🔧 [S049] Error creating regex analyzer:`, error);
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Initialize analyzer with semantic engine
58
+ */
59
+ async initialize(semanticEngine) {
60
+ this.semanticEngine = semanticEngine;
61
+
62
+ if (process.env.SUNLINT_DEBUG) {
63
+ console.log(`🔧 [S049] Main analyzer initializing...`);
64
+ }
65
+
66
+ // Initialize both analyzers
67
+ if (this.symbolAnalyzer) {
68
+ await this.symbolAnalyzer.initialize?.(semanticEngine);
69
+ }
70
+ if (this.regexAnalyzer) {
71
+ await this.regexAnalyzer.initialize?.(semanticEngine);
72
+ }
73
+
74
+ // Clean up if needed
75
+ if (this.regexAnalyzer) {
76
+ this.regexAnalyzer.cleanup?.();
77
+ }
78
+
79
+ if (process.env.SUNLINT_DEBUG) {
80
+ console.log(`🔧 [S049] Main analyzer initialized successfully`);
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Single file analysis method for testing
86
+ */
87
+ analyzeSingle(filePath, options = {}) {
88
+ if (process.env.SUNLINT_DEBUG) {
89
+ console.log(`📊 [S049] analyzeSingle() called for: ${filePath}`);
90
+ }
91
+
92
+ // Return result using same format as analyze method
93
+ return this.analyze([filePath], "typescript", options);
94
+ }
95
+
96
+ async analyze(files, language, options = {}) {
97
+ if (process.env.SUNLINT_DEBUG) {
98
+ console.log(
99
+ `🔧 [S049] analyze() method called with ${files.length} files, language: ${language}`
100
+ );
101
+ }
102
+
103
+ const results = [];
104
+ const preferredEngine = options.engine || "heuristic";
105
+
106
+ for (const filePath of files) {
107
+ let fileResults = [];
108
+
109
+ try {
110
+ // Determine analysis strategy
111
+ let useSymbolBased = this.config.useSymbolBased && this.semanticEngine;
112
+
113
+ if (preferredEngine === "regex") {
114
+ useSymbolBased = false;
115
+ }
116
+
117
+ // Primary analysis: Symbol-based (when available and enabled)
118
+ if (useSymbolBased && this.symbolAnalyzer) {
119
+ if (process.env.SUNLINT_DEBUG) {
120
+ console.log(`🔧 [S049] Using symbol-based analysis for: ${filePath}`);
121
+ }
122
+
123
+ try {
124
+ fileResults = await this.symbolAnalyzer.analyze(filePath, language, options);
125
+ } catch (error) {
126
+ console.error(`🔧 [S049] Symbol-based analysis failed:`, error);
127
+ fileResults = [];
128
+ }
129
+ }
130
+
131
+ // Fallback analysis: Regex-based
132
+ if ((!fileResults || fileResults.length === 0) && this.config.fallbackToRegex && this.regexAnalyzer) {
133
+ if (process.env.SUNLINT_DEBUG) {
134
+ console.log(`🔧 [S049] Using regex-based analysis for: ${filePath}`);
135
+ }
136
+
137
+ try {
138
+ fileResults = await this.regexAnalyzer.analyze(filePath, language, options);
139
+ } catch (error) {
140
+ console.error(`🔧 [S049] Regex-based analysis failed:`, error);
141
+ fileResults = [];
142
+ }
143
+ }
144
+
145
+ // Add file results to overall results
146
+ if (fileResults && fileResults.length > 0) {
147
+ results.push(...fileResults);
148
+ }
149
+
150
+ } catch (error) {
151
+ console.error(`🔧 [S049] Error analyzing file ${filePath}:`, error);
152
+ }
153
+ }
154
+
155
+ if (process.env.SUNLINT_DEBUG) {
156
+ console.log(`🔧 [S049] Analysis completed. Found ${results.length} issues.`);
157
+ }
158
+
159
+ return results;
160
+ }
161
+
162
+ /**
163
+ * Clean up resources
164
+ */
165
+ cleanup() {
166
+ if (this.symbolAnalyzer) {
167
+ this.symbolAnalyzer.cleanup?.();
168
+ }
169
+ if (this.regexAnalyzer) {
170
+ this.regexAnalyzer.cleanup?.();
171
+ }
172
+ }
173
+ }
174
+
175
+ module.exports = S049Analyzer;