@sun-asterisk/sunlint 1.3.18 → 1.3.19

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 (34) hide show
  1. package/config/rules/enhanced-rules-registry.json +77 -18
  2. package/core/cli-program.js +2 -1
  3. package/core/github-annotate-service.js +89 -0
  4. package/core/output-service.js +25 -0
  5. package/core/summary-report-service.js +30 -30
  6. package/package.json +3 -2
  7. package/rules/common/C014_dependency_injection/symbol-based-analyzer.js +392 -280
  8. package/rules/common/C017_constructor_logic/analyzer.js +137 -503
  9. package/rules/common/C017_constructor_logic/config.json +50 -0
  10. package/rules/common/C017_constructor_logic/symbol-based-analyzer.js +463 -0
  11. package/rules/security/S006_no_plaintext_recovery_codes/symbol-based-analyzer.js +463 -21
  12. package/rules/security/S011_secure_guid_generation/README.md +255 -0
  13. package/rules/security/S011_secure_guid_generation/analyzer.js +135 -0
  14. package/rules/security/S011_secure_guid_generation/config.json +56 -0
  15. package/rules/security/S011_secure_guid_generation/symbol-based-analyzer.js +609 -0
  16. package/rules/security/S028_file_upload_size_limits/README.md +537 -0
  17. package/rules/security/S028_file_upload_size_limits/analyzer.js +202 -0
  18. package/rules/security/S028_file_upload_size_limits/config.json +186 -0
  19. package/rules/security/S028_file_upload_size_limits/symbol-based-analyzer.js +530 -0
  20. package/rules/security/S041_session_token_invalidation/README.md +303 -0
  21. package/rules/security/S041_session_token_invalidation/analyzer.js +242 -0
  22. package/rules/security/S041_session_token_invalidation/config.json +175 -0
  23. package/rules/security/S041_session_token_invalidation/regex-based-analyzer.js +411 -0
  24. package/rules/security/S041_session_token_invalidation/symbol-based-analyzer.js +674 -0
  25. package/rules/security/S044_re_authentication_required/README.md +136 -0
  26. package/rules/security/S044_re_authentication_required/analyzer.js +242 -0
  27. package/rules/security/S044_re_authentication_required/config.json +161 -0
  28. package/rules/security/S044_re_authentication_required/regex-based-analyzer.js +329 -0
  29. package/rules/security/S044_re_authentication_required/symbol-based-analyzer.js +537 -0
  30. package/rules/security/S045_brute_force_protection/README.md +345 -0
  31. package/rules/security/S045_brute_force_protection/analyzer.js +336 -0
  32. package/rules/security/S045_brute_force_protection/config.json +139 -0
  33. package/rules/security/S045_brute_force_protection/symbol-based-analyzer.js +646 -0
  34. package/rules/common/C017_constructor_logic/semantic-analyzer.js +0 -340
@@ -870,6 +870,23 @@
870
870
  "status": "stable",
871
871
  "tags": ["security", "secrets", "hardcoded"]
872
872
  },
873
+ "S028": {
874
+ "name": "Limit upload file size and number of files per user",
875
+ "description": "File uploads must enforce size limits and file quantity limits to prevent resource exhaustion and DoS attacks. Both file size and number of files should be limited at the server-side.",
876
+ "category": "security",
877
+ "severity": "error",
878
+ "languages": ["typescript", "javascript", "java"],
879
+ "analyzer": "./rules/security/S028_file_upload_size_limits/analyzer.js",
880
+ "version": "1.0.0",
881
+ "status": "stable",
882
+ "tags": [
883
+ "security",
884
+ "file-upload",
885
+ "dos-prevention",
886
+ "resource-limits",
887
+ "owasp"
888
+ ]
889
+ },
873
890
  "S029": {
874
891
  "name": "Require CSRF Protection",
875
892
  "description": "Require CSRF protection for state-changing operations",
@@ -1079,16 +1096,27 @@
1079
1096
  }
1080
1097
  },
1081
1098
  "S041": {
1082
- "name": "Require Session Invalidate on Logout",
1083
- "description": "Require session invalidation on logout",
1099
+ "name": "Session Tokens must be invalidated after logout or expiration",
1100
+ "description": "Session tokens must be properly invalidated after logout or expiration to prevent session hijacking and unauthorized access. This includes clearing session data, invalidating JWT tokens, and ensuring proper session cleanup.",
1084
1101
  "category": "security",
1085
1102
  "severity": "error",
1086
1103
  "languages": ["typescript", "javascript"],
1087
- "analyzer": "eslint",
1088
- "eslintRule": "custom/typescript_s041",
1104
+ "analyzer": "./rules/security/S041_session_token_invalidation/analyzer.js",
1105
+ "config": "./rules/security/S041_session_token_invalidation/config.json",
1089
1106
  "version": "1.0.0",
1090
1107
  "status": "stable",
1091
- "tags": ["security", "session", "logout"]
1108
+ "tags": ["security", "session", "token", "logout", "invalidation", "owasp"],
1109
+ "strategy": {
1110
+ "preferred": "ast",
1111
+ "fallbacks": ["ast", "regex"],
1112
+ "accuracy": {
1113
+ "ast": 95,
1114
+ "regex": 85
1115
+ }
1116
+ },
1117
+ "engineMappings": {
1118
+ "heuristic": ["rules/security/S041_session_token_invalidation/analyzer.js"]
1119
+ }
1092
1120
  },
1093
1121
  "S042": {
1094
1122
  "name": "Require Periodic Reauthentication",
@@ -1115,28 +1143,49 @@
1115
1143
  "tags": ["security", "session", "password"]
1116
1144
  },
1117
1145
  "S044": {
1118
- "name": "Require Full Session for Sensitive Operations",
1119
- "description": "Require full session validation for sensitive operations",
1146
+ "name": "Re-authentication Required for Sensitive Operations",
1147
+ "description": "Require re-authentication before performing sensitive operations such as password changes, email changes, profile updates, and other critical account modifications. This prevents unauthorized access to sensitive account functions even if a session is compromised.",
1120
1148
  "category": "security",
1121
1149
  "severity": "error",
1122
1150
  "languages": ["typescript", "javascript"],
1123
- "analyzer": "eslint",
1124
- "eslintRule": "custom/typescript_s044",
1151
+ "analyzer": "./rules/security/S044_re_authentication_required/analyzer.js",
1152
+ "config": "./rules/security/S044_re_authentication_required/config.json",
1125
1153
  "version": "1.0.0",
1126
1154
  "status": "stable",
1127
- "tags": ["security", "session", "validation"]
1155
+ "tags": ["security", "authentication", "re-authentication", "sensitive-operations", "owasp"],
1156
+ "strategy": {
1157
+ "preferred": "ast",
1158
+ "fallbacks": ["ast", "regex"],
1159
+ "accuracy": {
1160
+ "ast": 95,
1161
+ "regex": 85
1162
+ }
1163
+ },
1164
+ "engineMappings": {
1165
+ "heuristic": ["rules/security/S044_re_authentication_required/analyzer.js"]
1166
+ }
1128
1167
  },
1129
1168
  "S045": {
1130
- "name": "Anti Automation Controls",
1131
- "description": "Implement anti-automation controls",
1169
+ "name": "Brute-force Protection",
1170
+ "description": "Implement protection against brute-force attacks on authentication endpoints. This rule detects missing rate limiting, account lockout mechanisms, and other brute-force protection measures in authentication flows.",
1132
1171
  "category": "security",
1133
- "severity": "warning",
1172
+ "severity": "error",
1134
1173
  "languages": ["typescript", "javascript"],
1135
- "analyzer": "eslint",
1136
- "eslintRule": "custom/typescript_s045",
1174
+ "analyzer": "./rules/security/S045_brute_force_protection/analyzer.js",
1175
+ "config": "./rules/security/S045_brute_force_protection/config.json",
1137
1176
  "version": "1.0.0",
1138
1177
  "status": "stable",
1139
- "tags": ["security", "automation", "protection"]
1178
+ "tags": ["security", "authentication", "brute-force", "rate-limiting", "owasp"],
1179
+ "strategy": {
1180
+ "preferred": "heuristic",
1181
+ "fallbacks": ["heuristic"],
1182
+ "accuracy": {
1183
+ "heuristic": 95
1184
+ }
1185
+ },
1186
+ "engineMappings": {
1187
+ "heuristic": "rules/security/S045_brute_force_protection/analyzer.js"
1188
+ }
1140
1189
  },
1141
1190
  "S046": {
1142
1191
  "name": "Secure Notification on Auth Change",
@@ -1239,8 +1288,16 @@
1239
1288
  "name": "One Behavior per Test (AAA Pattern)",
1240
1289
  "description": "Enforce single behavior testing - each test should verify exactly one action/behavior with clear Arrange-Act-Assert structure",
1241
1290
  "category": "common",
1242
- "severity": "warning",
1243
- "languages": ["typescript", "javascript", "java", "csharp", "swift", "kotlin", "python"],
1291
+ "severity": "warning",
1292
+ "languages": [
1293
+ "typescript",
1294
+ "javascript",
1295
+ "java",
1296
+ "csharp",
1297
+ "swift",
1298
+ "kotlin",
1299
+ "python"
1300
+ ],
1244
1301
  "analyzer": "./rules/common/C065_one_behavior_per_test/analyzer.js",
1245
1302
  "config": "./rules/common/C065_one_behavior_per_test/config.json",
1246
1303
  "version": "1.0.0",
@@ -1451,6 +1508,8 @@
1451
1508
  "category": "general",
1452
1509
  "severity": "warning",
1453
1510
  "languages": ["typescript", "javascript"],
1511
+ "analyzer": "./rules/common/C017_constructor_logic/analyzer.js",
1512
+ "config": "./rules/common/C017_constructor_logic/config.json",
1454
1513
  "version": "1.0.0",
1455
1514
  "status": "migrated",
1456
1515
  "tags": ["migrated"],
@@ -36,7 +36,8 @@ function createCliProgram() {
36
36
  .option('-o, --output <file>', 'Output file path')
37
37
  .option('--output-summary <file>', 'Output summary report file path (JSON format for CI/CD)')
38
38
  .option('--upload-report [url]', 'Upload summary report to API endpoint after analysis (default: Sun* Coding Standards API)')
39
- .option('--config <file>', 'Configuration file path (default: auto-discover)');
39
+ .option('--config <file>', 'Configuration file path (default: auto-discover)')
40
+ .option('--github-annotate', 'Annotate GitHub PR with results (requires --output and --format=json)');
40
41
 
41
42
  // File targeting options
42
43
  program
@@ -0,0 +1,89 @@
1
+ /**
2
+ * GitHub Annotate Service
3
+ * Äį»c file JSON kįŗæt quįŗ£ vĆ  comment annotation lĆŖn GitHub PR tʰʔng ứng
4
+ * Usage: githubAnnotateService.annotate({ jsonFile, githubToken, repo, prNumber })
5
+ */
6
+
7
+ const fs = require('fs');
8
+ let Octokit;
9
+
10
+ /**
11
+ * Annotate GitHub PR with SunLint results
12
+ * @param {Object} options
13
+ * @param {string} options.jsonFile - Path to JSON result file
14
+ * @param {string} options.githubToken - GitHub token (with repo:write)
15
+ * @param {string} options.repo - GitHub repo in format owner/repo
16
+ * @param {number} options.prNumber - Pull request number
17
+ */
18
+ async function annotate({ jsonFile, githubToken, repo, prNumber }) {
19
+ if (!fs.existsSync(jsonFile)) {
20
+ throw new Error(`Result file not found: ${jsonFile}`);
21
+ }
22
+ const raw = JSON.parse(fs.readFileSync(jsonFile, 'utf8'));
23
+ let violations = [];
24
+ if (Array.isArray(raw)) {
25
+ const cwd = process.env.GITHUB_WORKSPACE || process.cwd();
26
+ for (const fileObj of raw) {
27
+ if (fileObj && fileObj.filePath && Array.isArray(fileObj.messages)) {
28
+ let relPath = fileObj.filePath;
29
+ if (relPath.startsWith(cwd)) {
30
+ relPath = relPath.slice(cwd.length);
31
+ if (relPath.startsWith('/') || relPath.startsWith('\\')) relPath = relPath.slice(1);
32
+ }
33
+ for (const msg of fileObj.messages) {
34
+ violations.push({
35
+ file: relPath,
36
+ line: msg.line,
37
+ rule: msg.ruleId,
38
+ severity: msg.severity === 2 ? 'error' : 'warning',
39
+ message: msg.message
40
+ });
41
+ }
42
+ }
43
+ }
44
+ } else {
45
+ violations = raw.violations || [];
46
+ }
47
+ console.log(violations);
48
+ const token = githubToken || process.env.GITHUB_TOKEN;
49
+ if (!token) throw new Error('Missing GitHub token');
50
+ const [owner, repoName] = repo.split('/');
51
+ if (!Octokit) {
52
+ // Dynamic import Ä‘į»ƒ hį»— trợ ESM
53
+ Octokit = (await import('@octokit/rest')).Octokit;
54
+ }
55
+ const octokit = new Octokit({ auth: token });
56
+
57
+ const { data: prData } = await octokit.pulls.get({ owner, repo: repoName, pull_number: prNumber });
58
+ const head_sha = prData.head.sha;
59
+
60
+ const filesRes = await octokit.pulls.listFiles({ owner, repo: repoName, pull_number: prNumber });
61
+ const prFiles = filesRes.data.map(f => f.filename);
62
+
63
+ const reviewComments = violations
64
+ .filter(v => prFiles.includes(v.file))
65
+ .map(v => ({
66
+ path: v.file,
67
+ line: v.line,
68
+ side: 'RIGHT',
69
+ body: `[${v.rule}] ${v.message}`
70
+ }));
71
+
72
+ if (reviewComments.length === 0) return { message: 'No matching PR file violations to comment.' };
73
+
74
+ // Nįŗæu có error thƬ yĆŖu cįŗ§u thay đổi, khĆ“ng thƬ chỉ comment
75
+ const hasError = violations.some(v => v.severity === 'error');
76
+ const eventType = hasError ? 'REQUEST_CHANGES' : 'COMMENT';
77
+
78
+ const reviewRes = await octokit.pulls.createReview({
79
+ owner,
80
+ repo: repoName,
81
+ pull_number: prNumber,
82
+ commit_id: head_sha,
83
+ event: eventType,
84
+ comments: reviewComments
85
+ });
86
+ return reviewRes.data;
87
+ }
88
+
89
+ module.exports = { annotate };
@@ -34,6 +34,7 @@ class OutputService {
34
34
  }
35
35
  }
36
36
 
37
+
37
38
  async outputResults(results, options, metadata = {}) {
38
39
  // Generate report based on format
39
40
  const report = this.generateReport(results, metadata, options);
@@ -49,6 +50,30 @@ class OutputService {
49
50
  const content = typeof outputData === 'string' ? outputData : JSON.stringify(outputData, null, 2);
50
51
  fs.writeFileSync(options.output, content);
51
52
  console.log(chalk.green(`šŸ“„ Report saved to: ${options.output}`));
53
+
54
+ // Annotate GitHub nįŗæu đủ điều kiện
55
+ if (options.githubAnnotate && options.format === 'json') {
56
+ try {
57
+ const annotate = require('./github-annotate-service').annotate;
58
+ // Lįŗ„y cĆ”c biįŗæn mĆ“i trĘ°į»ng cįŗ§n thiįŗæt cho annotate
59
+ const repo = process.env.GITHUB_REPOSITORY || options.githubRepo;
60
+ const prNumber = process.env.GITHUB_PR_NUMBER ? parseInt(process.env.GITHUB_PR_NUMBER) : options.githubPrNumber;
61
+ const githubToken = process.env.GITHUB_TOKEN || options.githubToken;
62
+ if (repo && prNumber && githubToken) {
63
+ await annotate({
64
+ jsonFile: options.output,
65
+ githubToken,
66
+ repo,
67
+ prNumber
68
+ });
69
+ console.log(chalk.green('āœ… Annotated GitHub PR with SunLint results.'));
70
+ } else {
71
+ console.log(chalk.yellow('āš ļø Missing GITHUB_REPOSITORY, GITHUB_PR_NUMBER, or GITHUB_TOKEN for GitHub annotation.'));
72
+ }
73
+ } catch (err) {
74
+ console.log(chalk.red('āŒ Failed to annotate GitHub PR:'), err.message);
75
+ }
76
+ }
52
77
  }
53
78
 
54
79
  // Summary report output (new feature for CI/CD)
@@ -13,7 +13,7 @@ class SummaryReportService {
13
13
  // Load version from package.json
14
14
  this.version = this._loadVersion();
15
15
  }
16
-
16
+
17
17
  /**
18
18
  * Load version from package.json
19
19
  * @returns {string} Package version
@@ -23,9 +23,9 @@ class SummaryReportService {
23
23
  try {
24
24
  const packageJsonPath = path.join(__dirname, '..', 'package.json');
25
25
  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
26
- return packageJson.version || '1.3.18';
26
+ return packageJson.version || '1.3.19';
27
27
  } catch (error) {
28
- return '1.3.18'; // Fallback version
28
+ return '1.3.19'; // Fallback version
29
29
  }
30
30
  }
31
31
 
@@ -154,44 +154,44 @@ class SummaryReportService {
154
154
  const gitInfo = this.getGitInfo(options.cwd);
155
155
 
156
156
  // Override with environment variables if available (from CI/CD)
157
- const repository_url = process.env.GITHUB_REPOSITORY
157
+ const repository_url = process.env.GITHUB_REPOSITORY
158
158
  ? `https://github.com/${process.env.GITHUB_REPOSITORY}`
159
159
  : gitInfo.repository_url;
160
-
161
- const repository_name = process.env.GITHUB_REPOSITORY
160
+
161
+ const repository_name = process.env.GITHUB_REPOSITORY
162
162
  ? process.env.GITHUB_REPOSITORY.split('/')[1]
163
163
  : (gitInfo.repository_name || null);
164
-
164
+
165
165
  const branch = process.env.GITHUB_REF_NAME || gitInfo.branch;
166
166
  const commit_hash = process.env.GITHUB_SHA || gitInfo.commit_hash;
167
-
167
+
168
168
  // Get commit details from GitHub context or git
169
- const commit_message = process.env.GITHUB_EVENT_HEAD_COMMIT_MESSAGE
170
- || (process.env.GITHUB_EVENT_PATH
169
+ const commit_message = process.env.GITHUB_EVENT_HEAD_COMMIT_MESSAGE
170
+ || (process.env.GITHUB_EVENT_PATH
171
171
  ? this._getGitHubEventData('head_commit.message')
172
172
  : null)
173
173
  || gitInfo.commit_message;
174
-
174
+
175
175
  const author_email = process.env.GITHUB_EVENT_HEAD_COMMIT_AUTHOR_EMAIL
176
- || (process.env.GITHUB_EVENT_PATH
176
+ || (process.env.GITHUB_EVENT_PATH
177
177
  ? this._getGitHubEventData('head_commit.author.email')
178
178
  : null)
179
179
  || gitInfo.author_email;
180
-
180
+
181
181
  const author_name = process.env.GITHUB_EVENT_HEAD_COMMIT_AUTHOR_NAME
182
- || (process.env.GITHUB_EVENT_PATH
182
+ || (process.env.GITHUB_EVENT_PATH
183
183
  ? this._getGitHubEventData('head_commit.author.name')
184
184
  : null)
185
185
  || gitInfo.author_name;
186
-
186
+
187
187
  // Get PR number from GitHub event or git
188
188
  let pr_number = null;
189
189
  if (process.env.GITHUB_EVENT_PATH) {
190
- pr_number = this._getGitHubEventData('pull_request.number')
190
+ pr_number = this._getGitHubEventData('pull_request.number')
191
191
  || this._getGitHubEventData('number');
192
192
  }
193
193
  pr_number = pr_number || gitInfo.pr_number;
194
-
194
+
195
195
  // Get project path (for mono-repo support)
196
196
  const project_path = gitInfo.project_path;
197
197
 
@@ -203,20 +203,20 @@ class SummaryReportService {
203
203
  if (!violation || typeof violation !== 'object') {
204
204
  return; // Skip non-objects
205
205
  }
206
-
206
+
207
207
  // Skip objects that look like metadata/config (have nested objects like semanticEngine, project, etc.)
208
208
  if (violation.semanticEngine || violation.project || violation.options) {
209
209
  return; // Skip config objects
210
210
  }
211
-
211
+
212
212
  // Get ruleId from various possible fields
213
213
  const ruleId = violation.ruleId || violation.rule || 'unknown';
214
-
214
+
215
215
  // Ensure ruleId is a string (not an object)
216
216
  if (typeof ruleId !== 'string') {
217
217
  return; // Skip invalid ruleId
218
218
  }
219
-
219
+
220
220
  if (!violationsByRule[ruleId]) {
221
221
  violationsByRule[ruleId] = {
222
222
  rule_code: ruleId,
@@ -253,7 +253,7 @@ class SummaryReportService {
253
253
  sunlint_version: options.version || this.version,
254
254
  analysis_duration_ms: options.duration || 0,
255
255
  violations: violationsSummary,
256
-
256
+
257
257
  // Additional metadata for backwards compatibility
258
258
  metadata: {
259
259
  generated_at: new Date().toISOString(),
@@ -283,11 +283,11 @@ class SummaryReportService {
283
283
  if (!eventPath || !fs.existsSync(eventPath)) {
284
284
  return null;
285
285
  }
286
-
286
+
287
287
  const eventData = JSON.parse(fs.readFileSync(eventPath, 'utf8'));
288
288
  const keys = path.split('.');
289
289
  let value = eventData;
290
-
290
+
291
291
  for (const key of keys) {
292
292
  if (value && typeof value === 'object' && key in value) {
293
293
  value = value[key];
@@ -295,7 +295,7 @@ class SummaryReportService {
295
295
  return null;
296
296
  }
297
297
  }
298
-
298
+
299
299
  return value;
300
300
  } catch (error) {
301
301
  return null;
@@ -311,7 +311,7 @@ class SummaryReportService {
311
311
  */
312
312
  saveSummaryReport(violations, scoringSummary, outputPath, options = {}) {
313
313
  const summaryReport = this.generateSummaryReport(violations, scoringSummary, options);
314
-
314
+
315
315
  // Ensure directory exists
316
316
  const dir = path.dirname(outputPath);
317
317
  if (!fs.existsSync(dir)) {
@@ -320,7 +320,7 @@ class SummaryReportService {
320
320
 
321
321
  // Write to file with pretty formatting
322
322
  fs.writeFileSync(outputPath, JSON.stringify(summaryReport, null, 2), 'utf8');
323
-
323
+
324
324
  return summaryReport;
325
325
  }
326
326
 
@@ -340,7 +340,7 @@ class SummaryReportService {
340
340
  const warningCount = summaryReport.warning_count || 0;
341
341
  const violationsByRule = summaryReport.violations || [];
342
342
  const violationsPerKLOC = summaryReport.quality?.metrics?.violationsPerKLOC || 0;
343
-
343
+
344
344
  let output = '\nšŸ“Š Quality Summary Report\n';
345
345
  output += '━'.repeat(50) + '\n';
346
346
  output += `šŸ“ˆ Quality Score: ${score} (Grade: ${grade})\n`;
@@ -348,14 +348,14 @@ class SummaryReportService {
348
348
  output += `šŸ“ Lines of Code: ${linesOfCode.toLocaleString()}\n`;
349
349
  output += `āš ļø Total Violations: ${totalViolations} (${errorCount} errors, ${warningCount} warnings)\n`;
350
350
  output += `šŸ“Š Violations per KLOC: ${violationsPerKLOC}\n`;
351
-
351
+
352
352
  if (violationsByRule.length > 0) {
353
353
  output += '\nšŸ” Top Violations by Rule:\n';
354
354
  violationsByRule.slice(0, 10).forEach((item, index) => {
355
355
  output += ` ${index + 1}. ${item.rule_code}: ${item.count} violations (${item.severity})\n`;
356
356
  });
357
357
  }
358
-
358
+
359
359
  return output;
360
360
  }
361
361
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sun-asterisk/sunlint",
3
- "version": "1.3.18",
3
+ "version": "1.3.19",
4
4
  "description": "ā˜€ļø SunLint - Multi-language static analysis tool for code quality and security | Sun* Engineering Standards",
5
5
  "main": "cli.js",
6
6
  "bin": {
@@ -91,6 +91,7 @@
91
91
  },
92
92
  "dependencies": {
93
93
  "@babel/parser": "^7.25.8",
94
+ "@octokit/rest": "^22.0.0",
94
95
  "@typescript-eslint/eslint-plugin": "^8.38.0",
95
96
  "@typescript-eslint/parser": "^8.38.0",
96
97
  "chalk": "^4.1.2",
@@ -138,4 +139,4 @@
138
139
  "url": "https://github.com/sun-asterisk/engineer-excellence/issues"
139
140
  },
140
141
  "homepage": "https://github.com/sun-asterisk/engineer-excellence/tree/main/coding-quality/extensions/sunlint#readme"
141
- }
142
+ }