git-repo-analyzer-test 1.0.0
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/.github/copilot-instructions.md +108 -0
- package/.idea/aianalyzer.iml +9 -0
- package/.idea/misc.xml +6 -0
- package/.idea/modules.xml +8 -0
- package/.idea/vcs.xml +6 -0
- package/API_REFERENCE.md +244 -0
- package/ENHANCEMENTS.md +282 -0
- package/README.md +179 -0
- package/USAGE.md +189 -0
- package/analysis.txt +0 -0
- package/bin/cli.js +135 -0
- package/docs/SONARCLOUD_ANALYSIS_COVERED.md +144 -0
- package/docs/SonarCloud_Presentation_Points.md +81 -0
- package/docs/UI_IMPROVEMENTS.md +117 -0
- package/package-lock_cmd.json +542 -0
- package/package.json +44 -0
- package/package_command.json +16 -0
- package/public/analysis-options.json +31 -0
- package/public/images/README.txt +2 -0
- package/public/images/rws-logo.png +0 -0
- package/public/index.html +2433 -0
- package/repositories.example.txt +17 -0
- package/sample-repos.txt +20 -0
- package/src/analyzers/accessibility.js +47 -0
- package/src/analyzers/cicd-enhanced.js +113 -0
- package/src/analyzers/codeReview-enhanced.js +599 -0
- package/src/analyzers/codeReview-enhanced.js:Zone.Identifier +3 -0
- package/src/analyzers/codeReview.js +171 -0
- package/src/analyzers/codeReview.js:Zone.Identifier +3 -0
- package/src/analyzers/documentation-enhanced.js +137 -0
- package/src/analyzers/performance-enhanced.js +747 -0
- package/src/analyzers/performance-enhanced.js:Zone.Identifier +3 -0
- package/src/analyzers/performance.js +211 -0
- package/src/analyzers/performance.js:Zone.Identifier +3 -0
- package/src/analyzers/performance_cmd.js +216 -0
- package/src/analyzers/quality-enhanced.js +386 -0
- package/src/analyzers/quality-enhanced.js:Zone.Identifier +3 -0
- package/src/analyzers/quality.js +92 -0
- package/src/analyzers/quality.js:Zone.Identifier +3 -0
- package/src/analyzers/security-enhanced.js +512 -0
- package/src/analyzers/security-enhanced.js:Zone.Identifier +3 -0
- package/src/analyzers/snyk-ai.js:Zone.Identifier +3 -0
- package/src/analyzers/sonarcloud.js +928 -0
- package/src/analyzers/vulnerability.js +185 -0
- package/src/analyzers/vulnerability.js:Zone.Identifier +3 -0
- package/src/cli.js:Zone.Identifier +3 -0
- package/src/config.js +43 -0
- package/src/core/analyzerEngine.js +68 -0
- package/src/core/reportGenerator.js +21 -0
- package/src/gemini.js +321 -0
- package/src/github/client.js +124 -0
- package/src/github/client.js:Zone.Identifier +3 -0
- package/src/index.js +93 -0
- package/src/index_cmd.js +130 -0
- package/src/openai.js +297 -0
- package/src/report/generator.js +459 -0
- package/src/report/generator_cmd.js +459 -0
- package/src/report/pdf-generator.js +387 -0
- package/src/report/pdf-generator.js:Zone.Identifier +3 -0
- package/src/server.js +431 -0
- package/src/server.js:Zone.Identifier +3 -0
- package/src/server_cmd.js +434 -0
- package/src/sonarcloud/client.js +365 -0
- package/src/sonarcloud/scanner.js +171 -0
- package/src.zip +0 -0
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
import { table } from 'table';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
|
|
4
|
+
export class ReportGenerator {
|
|
5
|
+
/**
|
|
6
|
+
* Generate comprehensive analysis report
|
|
7
|
+
*/
|
|
8
|
+
static generateReport(repoName, analysis) {
|
|
9
|
+
const summary = this.generateSummary(analysis);
|
|
10
|
+
const report = {
|
|
11
|
+
timestamp: new Date().toISOString(),
|
|
12
|
+
repository: repoName,
|
|
13
|
+
summary,
|
|
14
|
+
/** Project overall: single score (0ā10), rating (A+āF), and 0ā100 scale for display */
|
|
15
|
+
overall: {
|
|
16
|
+
score: summary.overallScore,
|
|
17
|
+
score100: summary.overallScore100,
|
|
18
|
+
rating: summary.overallRating,
|
|
19
|
+
},
|
|
20
|
+
detailed: analysis,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
return report;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Generate text-based report for console output
|
|
28
|
+
*/
|
|
29
|
+
static generateTextReport(repoName, analysis) {
|
|
30
|
+
let output = '';
|
|
31
|
+
|
|
32
|
+
output += chalk.bold.cyan(`\nš GitHub Repository Analysis Report\n`);
|
|
33
|
+
output += chalk.gray(`Repository: ${repoName}\n`);
|
|
34
|
+
output += chalk.gray(`Generated: ${new Date().toLocaleString()}\n`);
|
|
35
|
+
|
|
36
|
+
// Overall Score
|
|
37
|
+
output += this.generateOverallScore(analysis);
|
|
38
|
+
|
|
39
|
+
// Quality Section (skip if Sonar-only run)
|
|
40
|
+
if (analysis.quality) output += this.generateQualitySection(analysis.quality);
|
|
41
|
+
|
|
42
|
+
// Security Section (skip if Sonar-only run)
|
|
43
|
+
if (analysis.security) output += this.generateSecuritySection(analysis.security);
|
|
44
|
+
|
|
45
|
+
// SonarCloud Section (always in final report)
|
|
46
|
+
if (analysis.sonarCloud) {
|
|
47
|
+
output += this.generateSonarCloudSection(analysis.sonarCloud);
|
|
48
|
+
} else {
|
|
49
|
+
output += chalk.bold.cyan(`\nš SonarCloud Code Quality\n`);
|
|
50
|
+
output += chalk.gray('Unavailable. Add SONAR_TOKEN and analyze a repo that exists on SonarCloud to include in the report.\n');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Code Review Section (skip if Sonar-only run)
|
|
54
|
+
if (analysis.codeReview) output += this.generateCodeReviewSection(analysis.codeReview);
|
|
55
|
+
|
|
56
|
+
// Performance Section (skip if Sonar-only run)
|
|
57
|
+
if (analysis.performance) output += this.generatePerformanceSection(analysis.performance);
|
|
58
|
+
|
|
59
|
+
// Summary & Recommendations
|
|
60
|
+
output += this.generateSummarySection(analysis);
|
|
61
|
+
|
|
62
|
+
return output;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Generate overall score visualization (includes SonarCloud when available).
|
|
67
|
+
* Supports Sonar-only runs when quality, security, codeReview, performance are undefined.
|
|
68
|
+
*/
|
|
69
|
+
static generateOverallScore(analysis) {
|
|
70
|
+
const scores = [];
|
|
71
|
+
if (analysis.quality?.score != null) scores.push(analysis.quality.score);
|
|
72
|
+
if (analysis.security?.score != null) scores.push(analysis.security.score);
|
|
73
|
+
if (analysis.codeReview?.score != null) scores.push(analysis.codeReview.score);
|
|
74
|
+
if (analysis.performance?.score != null) scores.push(analysis.performance.score);
|
|
75
|
+
if (analysis.sonarCloud?.available && analysis.sonarCloud.score != null) {
|
|
76
|
+
scores.push(analysis.sonarCloud.score);
|
|
77
|
+
}
|
|
78
|
+
if (scores.length === 0) {
|
|
79
|
+
return chalk.bold.yellow('\nšÆ Project Overall: Not available (no scores from any analyzer)\n');
|
|
80
|
+
}
|
|
81
|
+
const overallScore10 = scores.reduce((a, b) => a + b, 0) / scores.length;
|
|
82
|
+
const overallScore = Math.round(overallScore10);
|
|
83
|
+
const overallRating = this.getRating(overallScore);
|
|
84
|
+
|
|
85
|
+
let output = chalk.bold.yellow(`\nšÆ Project Overall: ${overallScore}/10 (${overallRating})\n`);
|
|
86
|
+
output += this.getScoreBar(Math.round(overallScore10 * 10)) + '\n';
|
|
87
|
+
|
|
88
|
+
return output;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Generate quality analysis section
|
|
93
|
+
*/
|
|
94
|
+
static generateQualitySection(quality) {
|
|
95
|
+
let output = chalk.bold.cyan(`\nā Code Quality Analysis\n`);
|
|
96
|
+
output += `Score: ${chalk.green(quality.score + '/10')} - Rating: ${chalk.bold(quality.rating)}\n\n`;
|
|
97
|
+
|
|
98
|
+
if (quality.issues && quality.issues.length > 0) {
|
|
99
|
+
output += chalk.bold(`Found ${quality.issues.length} Issue(s):\n`);
|
|
100
|
+
const issuesData = [['Severity', 'Type', 'Location', 'Recommendation']];
|
|
101
|
+
|
|
102
|
+
quality.issues.forEach((issue) => {
|
|
103
|
+
issuesData.push([
|
|
104
|
+
issue.severity,
|
|
105
|
+
issue.type,
|
|
106
|
+
issue.location,
|
|
107
|
+
issue.recommendation.substring(0, 50) + '...',
|
|
108
|
+
]);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
output += table(issuesData) + '\n';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (quality.recommendations && quality.recommendations.length > 0) {
|
|
115
|
+
output += chalk.bold(`Recommendations:\n`);
|
|
116
|
+
quality.recommendations.forEach((rec, idx) => {
|
|
117
|
+
output += `${idx + 1}. ${rec.action}\n`;
|
|
118
|
+
});
|
|
119
|
+
output += '\n';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return output;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Generate security section
|
|
127
|
+
*/
|
|
128
|
+
static generateSecuritySection(security) {
|
|
129
|
+
let output = chalk.bold.red(`\nš Security & Vulnerabilities Analysis\n`);
|
|
130
|
+
output += `Score: ${chalk.green(security.score + '/10')} - Rating: ${chalk.bold(security.rating)}\n`;
|
|
131
|
+
output += `Risk Level: ${this.getRiskLevelColor(security.riskLevel)(security.riskLevel)}\n\n`;
|
|
132
|
+
|
|
133
|
+
if (security.owasp10Vulnerabilities && security.owasp10Vulnerabilities.length > 0) {
|
|
134
|
+
output += chalk.bold(`OWASP Top 10 Findings (${security.owasp10Vulnerabilities.length}):\n`);
|
|
135
|
+
|
|
136
|
+
security.owasp10Vulnerabilities.forEach((vuln) => {
|
|
137
|
+
output += chalk.bold(`\n${vuln.rank}: ${vuln.title}\n`);
|
|
138
|
+
output += `Severity: ${this.getSeverityColor(vuln.severity)(vuln.severity)}\n`;
|
|
139
|
+
output += `Location: ${vuln.location}\n`;
|
|
140
|
+
output += `Description: ${vuln.description}\n`;
|
|
141
|
+
|
|
142
|
+
if (vuln.remediation && vuln.remediation.length > 0) {
|
|
143
|
+
output += chalk.yellow(`Remediation:\n`);
|
|
144
|
+
vuln.remediation.forEach((step, idx) => {
|
|
145
|
+
output += ` ${idx + 1}. ${step}\n`;
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
output += '\n';
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (security.recommendations && security.recommendations.length > 0) {
|
|
153
|
+
output += chalk.bold(`Recommendations:\n`);
|
|
154
|
+
security.recommendations.forEach((rec, idx) => {
|
|
155
|
+
output += `${idx + 1}. [${rec.priority}] ${rec.title}: ${rec.action}\n`;
|
|
156
|
+
});
|
|
157
|
+
output += '\n';
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return output;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Generate code review section
|
|
165
|
+
*/
|
|
166
|
+
static generateCodeReviewSection(codeReview) {
|
|
167
|
+
let output = chalk.bold.cyan(`\nš„ Code Review & Collaboration Analysis\n`);
|
|
168
|
+
output += `Score: ${chalk.green(codeReview.score + '/10')} - Rating: ${chalk.bold(codeReview.rating)}\n\n`;
|
|
169
|
+
|
|
170
|
+
const metricsData = [
|
|
171
|
+
['Metric', 'Value'],
|
|
172
|
+
['Total PRs', codeReview.reviewMetrics.totalPullRequests.toString()],
|
|
173
|
+
['Merged PRs', codeReview.reviewMetrics.mergedPullRequests.toString()],
|
|
174
|
+
['Open PRs', codeReview.reviewMetrics.openPullRequests.toString()],
|
|
175
|
+
['Closure Rate', codeReview.reviewMetrics.prClosureRate + '%'],
|
|
176
|
+
['Avg Review Time', codeReview.reviewMetrics.averageReviewTimeHours + ' hours'],
|
|
177
|
+
['Contributors', codeReview.collaborationMetrics.contributors.toString()],
|
|
178
|
+
];
|
|
179
|
+
|
|
180
|
+
output += table(metricsData) + '\n';
|
|
181
|
+
|
|
182
|
+
if (codeReview.codingStyle && codeReview.codingStyle.commitConventionAdherence) {
|
|
183
|
+
const adherence = codeReview.codingStyle.commitConventionAdherence;
|
|
184
|
+
output += chalk.bold(`Commit Convention Adherence:\n`);
|
|
185
|
+
output += `${adherence.percentage}% follow conventional format (${adherence.conventional}/${adherence.total})\n`;
|
|
186
|
+
output += `Status: ${adherence.status}\n\n`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (codeReview.recommendations && codeReview.recommendations.length > 0) {
|
|
190
|
+
output += chalk.bold(`Recommendations:\n`);
|
|
191
|
+
codeReview.recommendations.forEach((rec, idx) => {
|
|
192
|
+
output += `${idx + 1}. [${rec.priority}] ${rec.category}: ${rec.action}\n`;
|
|
193
|
+
});
|
|
194
|
+
output += '\n';
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return output;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Generate performance section
|
|
202
|
+
*/
|
|
203
|
+
static generatePerformanceSection(performance) {
|
|
204
|
+
let output = chalk.bold.cyan(`\nā” Performance & Release Velocity\n`);
|
|
205
|
+
output += `Score: ${chalk.green(performance.score + '/10')} - Rating: ${chalk.bold(performance.rating)}\n\n`;
|
|
206
|
+
|
|
207
|
+
const metricsData = [
|
|
208
|
+
['Metric', 'Value'],
|
|
209
|
+
['Total Releases', performance.releaseVelocity.totalReleases.toString()],
|
|
210
|
+
['Releases/Year', performance.releaseVelocity.averageReleasesPerYear.toString()],
|
|
211
|
+
['Release Cadence', performance.releaseVelocity.releaseCadence],
|
|
212
|
+
['Days Since Last Release', performance.releaseVelocity.daysSinceLastRelease?.toString() || 'N/A'],
|
|
213
|
+
['Total Commits', performance.developmentVelocity.totalCommits.toString()],
|
|
214
|
+
['Maintenance Level', performance.maintenancePattern.maintenanceLevel],
|
|
215
|
+
];
|
|
216
|
+
|
|
217
|
+
output += table(metricsData) + '\n';
|
|
218
|
+
|
|
219
|
+
if (performance.maintenancePattern) {
|
|
220
|
+
output += chalk.bold(`Maintenance Status:\n`);
|
|
221
|
+
output += `Level: ${performance.maintenancePattern.maintenanceLevel}\n`;
|
|
222
|
+
output += `Last Update: ${performance.maintenancePattern.lastUpdateIndicator}\n`;
|
|
223
|
+
output += `Status: ${performance.maintenancePattern.status}\n\n`;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (performance.recommendations && performance.recommendations.length > 0) {
|
|
227
|
+
output += chalk.bold(`Recommendations:\n`);
|
|
228
|
+
performance.recommendations.forEach((rec, idx) => {
|
|
229
|
+
output += `${idx + 1}. [${rec.priority}] ${rec.category}: ${rec.action}\n`;
|
|
230
|
+
});
|
|
231
|
+
output += '\n';
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return output;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Generate summary section. Supports Sonar-only runs (undefined quality/security/codeReview/performance).
|
|
239
|
+
*/
|
|
240
|
+
static generateSummarySection(analysis) {
|
|
241
|
+
let output = chalk.bold.cyan(`\nš Summary\n`);
|
|
242
|
+
|
|
243
|
+
const summary = this.generateSummary(analysis);
|
|
244
|
+
const summaryData = [['Category', 'Score', 'Rating', 'Status']];
|
|
245
|
+
if (analysis.quality) {
|
|
246
|
+
summaryData.push(['Code Quality', (summary.qualityScore != null ? summary.qualityScore + '/10' : 'Not available'), analysis.quality.rating || 'Not available', 'Complete']);
|
|
247
|
+
}
|
|
248
|
+
if (analysis.security) {
|
|
249
|
+
summaryData.push(['Security', (summary.securityScore != null ? summary.securityScore + '/10' : 'Not available'), analysis.security.rating || 'Not available', analysis.security.riskLevel || 'Not available']);
|
|
250
|
+
}
|
|
251
|
+
if (analysis.codeReview) {
|
|
252
|
+
summaryData.push(['Collaboration', (summary.collaborationScore != null ? summary.collaborationScore + '/10' : 'Not available'), analysis.codeReview.rating || 'Not available', 'Complete']);
|
|
253
|
+
}
|
|
254
|
+
if (analysis.performance) {
|
|
255
|
+
summaryData.push(['Performance', (summary.performanceScore != null ? summary.performanceScore + '/10' : 'Not available'), analysis.performance.rating || 'Not available', analysis.performance.maintenancePattern?.maintenanceLevel ?? 'Not available']);
|
|
256
|
+
}
|
|
257
|
+
if (analysis.sonarCloud) {
|
|
258
|
+
const sc = analysis.sonarCloud;
|
|
259
|
+
const scScore = sc.available && sc.score != null ? sc.score + '/10' : 'Not available';
|
|
260
|
+
const scRating = sc.rating || 'Not available';
|
|
261
|
+
const scStatus = sc.available ? (sc.qualityGate?.status || 'Complete') : (sc.unavailableReason || 'Not available');
|
|
262
|
+
summaryData.push(['SonarCloud', scScore, scRating, scStatus]);
|
|
263
|
+
}
|
|
264
|
+
output += table(summaryData) + '\n';
|
|
265
|
+
output += chalk.bold.green(`\nā Analysis Complete\n`);
|
|
266
|
+
|
|
267
|
+
return output;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Generate summary statistics (includes SonarCloud when available for final report).
|
|
272
|
+
* Supports Sonar-only runs when quality, security, codeReview, performance are undefined.
|
|
273
|
+
*/
|
|
274
|
+
static generateSummary(analysis) {
|
|
275
|
+
const scores = [];
|
|
276
|
+
if (analysis.quality?.score != null) scores.push(analysis.quality.score);
|
|
277
|
+
if (analysis.security?.score != null) scores.push(analysis.security.score);
|
|
278
|
+
if (analysis.codeReview?.score != null) scores.push(analysis.codeReview.score);
|
|
279
|
+
if (analysis.performance?.score != null) scores.push(analysis.performance.score);
|
|
280
|
+
if (analysis.sonarCloud?.available && analysis.sonarCloud.score != null) {
|
|
281
|
+
scores.push(analysis.sonarCloud.score);
|
|
282
|
+
}
|
|
283
|
+
if (scores.length === 0) {
|
|
284
|
+
return {
|
|
285
|
+
qualityScore: null,
|
|
286
|
+
securityScore: null,
|
|
287
|
+
collaborationScore: null,
|
|
288
|
+
performanceScore: null,
|
|
289
|
+
sonarCloudScore: null,
|
|
290
|
+
overallScore: null,
|
|
291
|
+
overallRating: null,
|
|
292
|
+
overallScore100: null,
|
|
293
|
+
healthStatus: 'Unavailable',
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
const overallScore10 = scores.reduce((a, b) => a + b, 0) / scores.length;
|
|
297
|
+
const overallScore = Math.round(overallScore10);
|
|
298
|
+
const overallRating = this.getRating(overallScore);
|
|
299
|
+
return {
|
|
300
|
+
qualityScore: analysis.quality?.score ?? null,
|
|
301
|
+
securityScore: analysis.security?.score ?? null,
|
|
302
|
+
collaborationScore: analysis.codeReview?.score ?? null,
|
|
303
|
+
performanceScore: analysis.performance?.score ?? null,
|
|
304
|
+
sonarCloudScore: analysis.sonarCloud?.available && analysis.sonarCloud.score != null ? analysis.sonarCloud.score : null,
|
|
305
|
+
overallScore,
|
|
306
|
+
overallRating,
|
|
307
|
+
overallScore100: Math.round(overallScore10 * 10),
|
|
308
|
+
healthStatus: overallScore >= 9 ? 'Excellent' : overallScore >= 7 ? 'Good' : overallScore >= 5 ? 'Fair' : 'Poor',
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Helper: Get rating from score
|
|
314
|
+
*/
|
|
315
|
+
static getRating(score) {
|
|
316
|
+
const n = Number(score);
|
|
317
|
+
if (Number.isNaN(n)) return 'Unavailable';
|
|
318
|
+
if (n >= 9) return 'A+';
|
|
319
|
+
if (n >= 8) return 'A';
|
|
320
|
+
if (n >= 7) return 'B+';
|
|
321
|
+
if (n >= 6) return 'B';
|
|
322
|
+
if (n >= 5) return 'C+';
|
|
323
|
+
if (n >= 4) return 'C';
|
|
324
|
+
return 'F';
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Helper: Get score bar visualization
|
|
329
|
+
*/
|
|
330
|
+
static getScoreBar(percentage) {
|
|
331
|
+
// Ensure percentage is in valid range
|
|
332
|
+
percentage = Math.max(0, Math.min(percentage, 100));
|
|
333
|
+
|
|
334
|
+
const filled = Math.max(0, Math.min(Math.round(percentage / 5), 20));
|
|
335
|
+
const empty = Math.max(0, 20 - filled);
|
|
336
|
+
const bar = 'ā'.repeat(Math.max(0, filled)) + 'ā'.repeat(Math.max(0, empty));
|
|
337
|
+
|
|
338
|
+
let color = chalk.green;
|
|
339
|
+
if (percentage < 40) color = chalk.red;
|
|
340
|
+
else if (percentage < 60) color = chalk.yellow;
|
|
341
|
+
else if (percentage < 80) color = chalk.cyan;
|
|
342
|
+
|
|
343
|
+
return color(`[${bar}] ${Math.round(percentage)}%`);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Helper: Get severity color
|
|
348
|
+
*/
|
|
349
|
+
static getSeverityColor(severity) {
|
|
350
|
+
switch (severity) {
|
|
351
|
+
case 'CRITICAL':
|
|
352
|
+
return chalk.bgRed.white;
|
|
353
|
+
case 'HIGH':
|
|
354
|
+
return chalk.red;
|
|
355
|
+
case 'MEDIUM':
|
|
356
|
+
return chalk.yellow;
|
|
357
|
+
case 'LOW':
|
|
358
|
+
return chalk.green;
|
|
359
|
+
default:
|
|
360
|
+
return chalk.gray;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Helper: Get risk level color
|
|
366
|
+
*/
|
|
367
|
+
static getRiskLevelColor(riskLevel) {
|
|
368
|
+
switch (riskLevel) {
|
|
369
|
+
case 'CRITICAL':
|
|
370
|
+
return chalk.bgRed.white;
|
|
371
|
+
case 'HIGH':
|
|
372
|
+
return chalk.red;
|
|
373
|
+
case 'MEDIUM':
|
|
374
|
+
return chalk.yellow;
|
|
375
|
+
case 'LOW':
|
|
376
|
+
return chalk.green;
|
|
377
|
+
default:
|
|
378
|
+
chalk.gray;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Generate SonarCloud code quality section.
|
|
384
|
+
* Data is our combined summary from free-plan APIs (quality gate + measures/component); no single "Overall Code" API.
|
|
385
|
+
*/
|
|
386
|
+
static generateSonarCloudSection(sonarData) {
|
|
387
|
+
let output = '';
|
|
388
|
+
output += chalk.bold.cyan(`\nš SonarCloud Code Quality (from API: Quality Gate + metrics)\n`);
|
|
389
|
+
if (!sonarData.available) {
|
|
390
|
+
output += chalk.gray(`Unavailable. ${sonarData.unavailableReason || 'Configure SONAR_TOKEN and add project on SonarCloud.'}\n`);
|
|
391
|
+
return output;
|
|
392
|
+
}
|
|
393
|
+
const summary = sonarData.overallSummary;
|
|
394
|
+
if (summary) {
|
|
395
|
+
output += chalk.gray(`Overall: ${summary.status === 'Passed' ? chalk.green(summary.status) : summary.status === 'Failed' ? chalk.red(summary.status) : summary.status}\n`);
|
|
396
|
+
}
|
|
397
|
+
output += chalk.gray(`Score: ${sonarData.score != null ? sonarData.score + '/10' : 'Not available'} - Rating: ${sonarData.rating || 'Not available'}\n`);
|
|
398
|
+
// output += chalk.gray(`Quality Gate: ${sonarData.qualityGate?.status === 'OK' ? chalk.green('Passed') : sonarData.qualityGate?.status === 'ERROR' ? chalk.red('Failed') : 'Unknown'}\n`);
|
|
399
|
+
if (sonarData.projectKey) output += chalk.gray(`Project: ${sonarData.projectKey}\n`);
|
|
400
|
+
const m = sonarData.metrics || {};
|
|
401
|
+
const metricsData = [
|
|
402
|
+
['Metric', 'Value'],
|
|
403
|
+
['Lines of Code', (m.ncloc ?? m.lines ?? 0).toString()],
|
|
404
|
+
['Bugs', (m.bugs ?? 0).toString()],
|
|
405
|
+
['Vulnerabilities', (m.vulnerabilities ?? 0).toString()],
|
|
406
|
+
// ['Code Smells', (m.codeSmells ?? 0).toString()],
|
|
407
|
+
['Security Hotspots', (m.securityHotspots ?? 0).toString()],
|
|
408
|
+
['Duplication', (m.duplicatedLinesDensity != null ? m.duplicatedLinesDensity + '%' : 'N/A')],
|
|
409
|
+
['Coverage', m.coverage != null ? m.coverage + '%' : 'N/A'],
|
|
410
|
+
['Complexity', (m.complexity ?? 0).toString()],
|
|
411
|
+
];
|
|
412
|
+
output += table(metricsData) + '\n';
|
|
413
|
+
|
|
414
|
+
if (sonarData.issues?.items?.length > 0) {
|
|
415
|
+
output += chalk.bold(`Issues (first 5 of ${sonarData.issues.total} total):\n`);
|
|
416
|
+
sonarData.issues.items.slice(0, 5).forEach((issue) => {
|
|
417
|
+
output += ` [${issue.severity}] ${issue.type}: ${issue.message?.substring(0, 60)}...\n`;
|
|
418
|
+
});
|
|
419
|
+
output += '\n';
|
|
420
|
+
}
|
|
421
|
+
if (sonarData.recommendations?.length > 0) {
|
|
422
|
+
output += chalk.bold(`Recommendations:\n`);
|
|
423
|
+
sonarData.recommendations.forEach((rec, idx) => {
|
|
424
|
+
output += `${idx + 1}. [${rec.priority}] ${rec.category}: ${rec.action}\n`;
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
return output;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Convert CVSS score to severity level
|
|
432
|
+
*/
|
|
433
|
+
static cvssToSeverity(cvss) {
|
|
434
|
+
if (cvss >= 9) return 'CRITICAL';
|
|
435
|
+
if (cvss >= 7) return 'HIGH';
|
|
436
|
+
if (cvss >= 4) return 'MEDIUM';
|
|
437
|
+
return 'LOW';
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Get color function for severity
|
|
442
|
+
*/
|
|
443
|
+
static getSeverityColor(severity) {
|
|
444
|
+
switch (severity?.toUpperCase()) {
|
|
445
|
+
case 'CRITICAL':
|
|
446
|
+
return chalk.bgRed.white;
|
|
447
|
+
case 'HIGH':
|
|
448
|
+
return chalk.red;
|
|
449
|
+
case 'MEDIUM':
|
|
450
|
+
return chalk.yellow;
|
|
451
|
+
case 'LOW':
|
|
452
|
+
return chalk.green;
|
|
453
|
+
default:
|
|
454
|
+
return chalk.gray;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
export default ReportGenerator;
|