@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.
- package/config/rules/enhanced-rules-registry.json +77 -18
- package/core/analysis-orchestrator.js +11 -3
- package/core/cli-program.js +2 -1
- package/core/github-annotate-service.js +89 -0
- package/core/output-service.js +52 -9
- package/core/summary-report-service.js +45 -27
- package/package.json +3 -2
- package/rules/common/C014_dependency_injection/symbol-based-analyzer.js +392 -280
- package/rules/common/C017_constructor_logic/analyzer.js +137 -503
- package/rules/common/C017_constructor_logic/config.json +50 -0
- package/rules/common/C017_constructor_logic/symbol-based-analyzer.js +463 -0
- package/rules/common/C047_no_duplicate_retry_logic/analyzer.js +96 -40
- package/rules/common/C047_no_duplicate_retry_logic/symbol-analyzer-enhanced.js +17 -2
- package/rules/security/S006_no_plaintext_recovery_codes/symbol-based-analyzer.js +463 -21
- package/rules/security/S011_secure_guid_generation/README.md +255 -0
- package/rules/security/S011_secure_guid_generation/analyzer.js +135 -0
- package/rules/security/S011_secure_guid_generation/config.json +56 -0
- package/rules/security/S011_secure_guid_generation/symbol-based-analyzer.js +609 -0
- package/rules/security/S028_file_upload_size_limits/README.md +537 -0
- package/rules/security/S028_file_upload_size_limits/analyzer.js +202 -0
- package/rules/security/S028_file_upload_size_limits/config.json +186 -0
- package/rules/security/S028_file_upload_size_limits/symbol-based-analyzer.js +530 -0
- package/rules/security/S041_session_token_invalidation/README.md +303 -0
- package/rules/security/S041_session_token_invalidation/analyzer.js +242 -0
- package/rules/security/S041_session_token_invalidation/config.json +175 -0
- package/rules/security/S041_session_token_invalidation/regex-based-analyzer.js +411 -0
- package/rules/security/S041_session_token_invalidation/symbol-based-analyzer.js +674 -0
- package/rules/security/S044_re_authentication_required/README.md +136 -0
- package/rules/security/S044_re_authentication_required/analyzer.js +242 -0
- package/rules/security/S044_re_authentication_required/config.json +161 -0
- package/rules/security/S044_re_authentication_required/regex-based-analyzer.js +329 -0
- package/rules/security/S044_re_authentication_required/symbol-based-analyzer.js +537 -0
- package/rules/security/S045_brute_force_protection/README.md +345 -0
- package/rules/security/S045_brute_force_protection/analyzer.js +336 -0
- package/rules/security/S045_brute_force_protection/config.json +139 -0
- package/rules/security/S045_brute_force_protection/symbol-based-analyzer.js +646 -0
- package/docs/CONSTANTS-ARCHITECTURE.md +0 -288
- package/docs/DEPLOYMENT-STRATEGIES.md +0 -270
- package/docs/ESLINT_INTEGRATION.md +0 -238
- package/docs/PERFORMANCE_MIGRATION_GUIDE.md +0 -368
- package/docs/PERFORMANCE_OPTIMIZATION_PLAN.md +0 -255
- 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": "
|
|
1083
|
-
"description": "
|
|
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": "
|
|
1088
|
-
"
|
|
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": "
|
|
1119
|
-
"description": "Require
|
|
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": "
|
|
1124
|
-
"
|
|
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", "
|
|
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": "
|
|
1131
|
-
"description": "Implement
|
|
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": "
|
|
1172
|
+
"severity": "error",
|
|
1134
1173
|
"languages": ["typescript", "javascript"],
|
|
1135
|
-
"analyzer": "
|
|
1136
|
-
"
|
|
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", "
|
|
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": [
|
|
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
|
-
|
|
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
|
package/core/cli-program.js
CHANGED
|
@@ -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 };
|
package/core/output-service.js
CHANGED
|
@@ -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.
|
|
31
|
+
return packageJson.version || '1.3.18';
|
|
32
32
|
} catch (error) {
|
|
33
|
-
return '1.3.
|
|
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
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
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.
|
|
26
|
+
return packageJson.version || '1.3.19';
|
|
27
27
|
} catch (error) {
|
|
28
|
-
return '1.3.
|
|
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
|
-
|
|
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.
|
|
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
|
+
}
|