@sun-asterisk/sunlint 1.3.17 → 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 (42) hide show
  1. package/config/rules/enhanced-rules-registry.json +77 -18
  2. package/core/analysis-orchestrator.js +11 -3
  3. package/core/cli-program.js +2 -1
  4. package/core/github-annotate-service.js +89 -0
  5. package/core/output-service.js +52 -9
  6. package/core/summary-report-service.js +45 -27
  7. package/package.json +3 -2
  8. package/rules/common/C014_dependency_injection/symbol-based-analyzer.js +392 -280
  9. package/rules/common/C017_constructor_logic/analyzer.js +137 -503
  10. package/rules/common/C017_constructor_logic/config.json +50 -0
  11. package/rules/common/C017_constructor_logic/symbol-based-analyzer.js +463 -0
  12. package/rules/common/C047_no_duplicate_retry_logic/analyzer.js +96 -40
  13. package/rules/common/C047_no_duplicate_retry_logic/symbol-analyzer-enhanced.js +17 -2
  14. package/rules/security/S006_no_plaintext_recovery_codes/symbol-based-analyzer.js +463 -21
  15. package/rules/security/S011_secure_guid_generation/README.md +255 -0
  16. package/rules/security/S011_secure_guid_generation/analyzer.js +135 -0
  17. package/rules/security/S011_secure_guid_generation/config.json +56 -0
  18. package/rules/security/S011_secure_guid_generation/symbol-based-analyzer.js +609 -0
  19. package/rules/security/S028_file_upload_size_limits/README.md +537 -0
  20. package/rules/security/S028_file_upload_size_limits/analyzer.js +202 -0
  21. package/rules/security/S028_file_upload_size_limits/config.json +186 -0
  22. package/rules/security/S028_file_upload_size_limits/symbol-based-analyzer.js +530 -0
  23. package/rules/security/S041_session_token_invalidation/README.md +303 -0
  24. package/rules/security/S041_session_token_invalidation/analyzer.js +242 -0
  25. package/rules/security/S041_session_token_invalidation/config.json +175 -0
  26. package/rules/security/S041_session_token_invalidation/regex-based-analyzer.js +411 -0
  27. package/rules/security/S041_session_token_invalidation/symbol-based-analyzer.js +674 -0
  28. package/rules/security/S044_re_authentication_required/README.md +136 -0
  29. package/rules/security/S044_re_authentication_required/analyzer.js +242 -0
  30. package/rules/security/S044_re_authentication_required/config.json +161 -0
  31. package/rules/security/S044_re_authentication_required/regex-based-analyzer.js +329 -0
  32. package/rules/security/S044_re_authentication_required/symbol-based-analyzer.js +537 -0
  33. package/rules/security/S045_brute_force_protection/README.md +345 -0
  34. package/rules/security/S045_brute_force_protection/analyzer.js +336 -0
  35. package/rules/security/S045_brute_force_protection/config.json +139 -0
  36. package/rules/security/S045_brute_force_protection/symbol-based-analyzer.js +646 -0
  37. package/docs/CONSTANTS-ARCHITECTURE.md +0 -288
  38. package/docs/DEPLOYMENT-STRATEGIES.md +0 -270
  39. package/docs/ESLINT_INTEGRATION.md +0 -238
  40. package/docs/PERFORMANCE_MIGRATION_GUIDE.md +0 -368
  41. package/docs/PERFORMANCE_OPTIMIZATION_PLAN.md +0 -255
  42. 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"],
@@ -547,9 +547,17 @@ class AnalysisOrchestrator {
547
547
  for (const engineResult of engineResults) {
548
548
  uniqueEngines.add(engineResult.engine);
549
549
 
550
- // Add engine-specific results
551
- if (engineResult.results) {
552
- mergedResults.results.push(...engineResult.results);
550
+ // Add engine-specific results with validation
551
+ if (engineResult.results && Array.isArray(engineResult.results)) {
552
+ // Filter out invalid entries (non-objects or config objects)
553
+ const validResults = engineResult.results.filter(result => {
554
+ if (!result || typeof result !== 'object') return false;
555
+ // Skip objects that look like metadata/config
556
+ if (result.semanticEngine || result.project || result._context) return false;
557
+ // Must have either file/filePath or be a valid result object
558
+ return result.file || result.filePath || result.violations || result.messages;
559
+ });
560
+ mergedResults.results.push(...validResults);
553
561
  }
554
562
 
555
563
  // Track engine statistics
@@ -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 };
@@ -28,12 +28,13 @@ class OutputService {
28
28
  try {
29
29
  const packageJsonPath = path.join(__dirname, '..', 'package.json');
30
30
  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
31
- return packageJson.version || '1.3.16';
31
+ return packageJson.version || '1.3.18';
32
32
  } catch (error) {
33
- return '1.3.16'; // Fallback version
33
+ return '1.3.18'; // Fallback version
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)
@@ -140,6 +165,17 @@ class OutputService {
140
165
  const allViolations = [];
141
166
  let totalFiles = results.filesAnalyzed || results.summary?.totalFiles || results.totalFiles || results.fileCount || 0;
142
167
 
168
+ // Helper function to validate violation object
169
+ const isValidViolation = (violation) => {
170
+ if (!violation || typeof violation !== 'object') return false;
171
+ // Skip config/metadata objects (have nested objects like semanticEngine, project, etc.)
172
+ if (violation.semanticEngine || violation.project || violation._context) return false;
173
+ // Must have ruleId as string
174
+ const ruleId = violation.ruleId || violation.rule;
175
+ if (!ruleId || typeof ruleId !== 'string') return false;
176
+ return true;
177
+ };
178
+
143
179
  // Collect all violations - handle both file-based and rule-based results
144
180
  if (results.results) {
145
181
  results.results.forEach(result => {
@@ -147,16 +183,20 @@ class OutputService {
147
183
  // Handle rule-based format (MultiRuleRunner)
148
184
  if (result.ruleId) {
149
185
  result.violations.forEach(violation => {
150
- allViolations.push(violation); // violation already has file path
186
+ if (isValidViolation(violation)) {
187
+ allViolations.push(violation); // violation already has file path
188
+ }
151
189
  });
152
190
  }
153
191
  // Handle file-based format (legacy)
154
192
  else {
155
193
  result.violations.forEach(violation => {
156
- allViolations.push({
157
- ...violation,
158
- file: result.filePath || result.file // Use filePath first, then file
159
- });
194
+ if (isValidViolation(violation)) {
195
+ allViolations.push({
196
+ ...violation,
197
+ file: result.filePath || result.file // Use filePath first, then file
198
+ });
199
+ }
160
200
  });
161
201
  }
162
202
  }
@@ -164,7 +204,7 @@ class OutputService {
164
204
  // Handle ESLint format (messages array)
165
205
  if (result.messages) {
166
206
  result.messages.forEach(message => {
167
- allViolations.push({
207
+ const violation = {
168
208
  file: result.filePath || message.file,
169
209
  ruleId: message.ruleId,
170
210
  severity: message.severity === 2 ? 'error' : 'warning',
@@ -172,7 +212,10 @@ class OutputService {
172
212
  line: message.line,
173
213
  column: message.column,
174
214
  source: message.source || 'eslint'
175
- });
215
+ };
216
+ if (isValidViolation(violation)) {
217
+ allViolations.push(violation);
218
+ }
176
219
  });
177
220
  }
178
221
  });
@@ -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.16';
26
+ return packageJson.version || '1.3.19';
27
27
  } catch (error) {
28
- return '1.3.16'; // Fallback version
28
+ return '1.3.19'; // Fallback version
29
29
  }
30
30
  }
31
31
 
@@ -154,51 +154,69 @@ 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
 
198
198
  // Count violations by rule
199
199
  const violationsByRule = {};
200
200
  violations.forEach(violation => {
201
- const ruleId = violation.ruleId || 'unknown';
201
+ // Validate that this is actually a violation object (not metadata/config)
202
+ // A valid violation should have ruleId/rule as string and message
203
+ if (!violation || typeof violation !== 'object') {
204
+ return; // Skip non-objects
205
+ }
206
+
207
+ // Skip objects that look like metadata/config (have nested objects like semanticEngine, project, etc.)
208
+ if (violation.semanticEngine || violation.project || violation.options) {
209
+ return; // Skip config objects
210
+ }
211
+
212
+ // Get ruleId from various possible fields
213
+ const ruleId = violation.ruleId || violation.rule || 'unknown';
214
+
215
+ // Ensure ruleId is a string (not an object)
216
+ if (typeof ruleId !== 'string') {
217
+ return; // Skip invalid ruleId
218
+ }
219
+
202
220
  if (!violationsByRule[ruleId]) {
203
221
  violationsByRule[ruleId] = {
204
222
  rule_code: ruleId,
@@ -235,7 +253,7 @@ class SummaryReportService {
235
253
  sunlint_version: options.version || this.version,
236
254
  analysis_duration_ms: options.duration || 0,
237
255
  violations: violationsSummary,
238
-
256
+
239
257
  // Additional metadata for backwards compatibility
240
258
  metadata: {
241
259
  generated_at: new Date().toISOString(),
@@ -265,11 +283,11 @@ class SummaryReportService {
265
283
  if (!eventPath || !fs.existsSync(eventPath)) {
266
284
  return null;
267
285
  }
268
-
286
+
269
287
  const eventData = JSON.parse(fs.readFileSync(eventPath, 'utf8'));
270
288
  const keys = path.split('.');
271
289
  let value = eventData;
272
-
290
+
273
291
  for (const key of keys) {
274
292
  if (value && typeof value === 'object' && key in value) {
275
293
  value = value[key];
@@ -277,7 +295,7 @@ class SummaryReportService {
277
295
  return null;
278
296
  }
279
297
  }
280
-
298
+
281
299
  return value;
282
300
  } catch (error) {
283
301
  return null;
@@ -293,7 +311,7 @@ class SummaryReportService {
293
311
  */
294
312
  saveSummaryReport(violations, scoringSummary, outputPath, options = {}) {
295
313
  const summaryReport = this.generateSummaryReport(violations, scoringSummary, options);
296
-
314
+
297
315
  // Ensure directory exists
298
316
  const dir = path.dirname(outputPath);
299
317
  if (!fs.existsSync(dir)) {
@@ -302,7 +320,7 @@ class SummaryReportService {
302
320
 
303
321
  // Write to file with pretty formatting
304
322
  fs.writeFileSync(outputPath, JSON.stringify(summaryReport, null, 2), 'utf8');
305
-
323
+
306
324
  return summaryReport;
307
325
  }
308
326
 
@@ -322,7 +340,7 @@ class SummaryReportService {
322
340
  const warningCount = summaryReport.warning_count || 0;
323
341
  const violationsByRule = summaryReport.violations || [];
324
342
  const violationsPerKLOC = summaryReport.quality?.metrics?.violationsPerKLOC || 0;
325
-
343
+
326
344
  let output = '\nšŸ“Š Quality Summary Report\n';
327
345
  output += '━'.repeat(50) + '\n';
328
346
  output += `šŸ“ˆ Quality Score: ${score} (Grade: ${grade})\n`;
@@ -330,14 +348,14 @@ class SummaryReportService {
330
348
  output += `šŸ“ Lines of Code: ${linesOfCode.toLocaleString()}\n`;
331
349
  output += `āš ļø Total Violations: ${totalViolations} (${errorCount} errors, ${warningCount} warnings)\n`;
332
350
  output += `šŸ“Š Violations per KLOC: ${violationsPerKLOC}\n`;
333
-
351
+
334
352
  if (violationsByRule.length > 0) {
335
353
  output += '\nšŸ” Top Violations by Rule:\n';
336
354
  violationsByRule.slice(0, 10).forEach((item, index) => {
337
355
  output += ` ${index + 1}. ${item.rule_code}: ${item.count} violations (${item.severity})\n`;
338
356
  });
339
357
  }
340
-
358
+
341
359
  return output;
342
360
  }
343
361
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sun-asterisk/sunlint",
3
- "version": "1.3.17",
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
+ }