sonarqube-issues-exporter
Version:
Enterprise-level SonarQube issues exporter with TypeScript support for generating comprehensive HTML reports with dark theme
299 lines • 12.2 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SonarQubeService = void 0;
const axios_1 = __importDefault(require("axios"));
const utils_1 = require("../utils");
class SonarQubeService {
api;
logger = (0, utils_1.getLogger)();
config;
constructor(config) {
this.config = config;
this.api = axios_1.default.create({
baseURL: config.url,
auth: {
username: config.token,
password: '',
},
timeout: 30000,
});
// Add request interceptor for logging
this.api.interceptors.request.use((config) => {
// Only log URL and method, not the full config which may contain sensitive data
this.logger.debug(`Making ${config.method?.toUpperCase()} request to: ${config.url}`);
return config;
}, (error) => {
this.logger.error('Request error:', error.message || error);
return Promise.reject(error instanceof Error ? error : new Error(String(error)));
});
// Add response interceptor for error handling
this.api.interceptors.response.use((response) => response, (error) => {
this.handleApiError(error);
return Promise.reject(error);
});
}
handleApiError(error) {
if (error.response?.status === 401) {
this.logger.error('Authentication failed (401 Unauthorized)');
this.logger.error('Possible causes:');
this.logger.error('- Invalid or expired token');
this.logger.error('- Insufficient permissions');
this.logger.error('- Incorrect SonarQube URL');
}
else if (error.response?.status === 403) {
this.logger.error('Access forbidden (403 Forbidden)');
this.logger.error('The token does not have sufficient permissions');
}
else if (error.response?.status === 404) {
this.logger.error('Project not found (404 Not Found)');
this.logger.error(`Project key '${this.config.projectKey}' may be incorrect`);
}
else {
this.logger.error(`API request failed: ${error.message}`);
}
}
async fetchAllIssues(options = {}) {
const { pageSize = 500, maxIssues = 10000, excludeStatuses = ['CLOSED'], includeResolvedIssues = false, onProgress, } = options;
let allIssues = [];
let page = 1;
let totalFetched = 0;
this.logger.info(`Starting to fetch issues for project: ${this.config.projectKey}`);
while (totalFetched < maxIssues) {
try {
const response = await this.api.get('/api/issues/search', {
params: {
componentKeys: this.config.projectKey,
organization: this.config.organization,
ps: Math.min(pageSize, maxIssues - totalFetched),
p: page,
statuses: includeResolvedIssues
? undefined
: ['OPEN', 'CONFIRMED', 'REOPENED'].join(','),
},
});
const { issues, paging } = response.data;
// Filter out excluded statuses
const filteredIssues = issues.filter((issue) => !excludeStatuses.includes(issue.status));
allIssues = allIssues.concat(filteredIssues);
totalFetched += filteredIssues.length;
this.logger.debug(`Fetched page ${page}: ${filteredIssues.length} issues (total: ${totalFetched})`);
if (onProgress) {
onProgress(totalFetched, Math.min(paging.total, maxIssues));
}
// Check if we've fetched all available issues
if (page * pageSize >= paging.total || issues.length === 0) {
break;
}
page++;
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
this.logger.error(`Failed to fetch page ${page}: ${errorMessage}`);
throw error;
}
}
this.logger.info(`Successfully fetched ${allIssues.length} issues`);
return allIssues;
}
async validateConnection() {
try {
await this.api.get('/api/system/status');
this.logger.info('SonarQube connection validated successfully');
return true;
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
this.logger.error('Failed to validate SonarQube connection:', errorMessage);
return false;
}
}
async getProjectInfo() {
try {
const response = await this.api.get('/api/projects/search', {
params: {
projects: this.config.projectKey,
organization: this.config.organization,
},
});
return response.data.components[0] || null;
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
this.logger.error('Failed to fetch project info:', errorMessage);
return null;
}
}
/**
* Fetch quality gate status for the project
*/
async getQualityGateStatus() {
try {
this.logger.info('Fetching quality gate status...');
const response = await this.api.get('/api/qualitygates/project_status', {
params: {
projectKey: this.config.projectKey,
organization: this.config.organization,
},
});
const data = response.data.projectStatus;
return {
status: data.status,
conditions: data.conditions?.map((condition) => ({
metric: condition.metricKey,
operator: condition.comparator,
value: condition.periodValue || condition.value,
errorThreshold: condition.errorThreshold,
warningThreshold: condition.warningThreshold,
actualValue: condition.actualValue,
status: condition.status,
})) || [],
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
this.logger.warn('Failed to fetch quality gate status:', errorMessage);
return {
status: 'NONE',
conditions: [],
};
}
}
/**
* Fetch project measures (code coverage, technical debt, etc.)
*/
async getProjectMeasures() {
try {
this.logger.info('Fetching project measures...');
const metrics = [
'coverage',
'duplicated_lines_density',
'ncloc',
'sqale_index',
'sqale_rating',
'reliability_rating',
'security_rating',
'complexity',
'sqale_debt_ratio',
];
const response = await this.api.get('/api/measures/component', {
params: {
component: this.config.projectKey,
organization: this.config.organization,
metricKeys: metrics.join(','),
},
});
const measures = response.data.component.measures;
const result = {};
for (const measure of measures) {
switch (measure.metric) {
case 'coverage':
result.coverage = parseFloat(measure.value || '0');
break;
case 'duplicated_lines_density':
result.duplicatedLinesDensity = parseFloat(measure.value || '0');
break;
case 'ncloc':
result.linesOfCode = parseInt(measure.value || '0', 10);
break;
case 'sqale_index':
result.technicalDebt = this.formatTechnicalDebt(parseInt(measure.value || '0', 10));
break;
case 'sqale_rating':
result.maintainabilityRating = this.formatRating(measure.value);
break;
case 'reliability_rating':
result.reliabilityRating = this.formatRating(measure.value);
break;
case 'security_rating':
result.securityRating = this.formatRating(measure.value);
break;
case 'complexity':
result.complexity = parseInt(measure.value || '0', 10);
break;
}
}
return result;
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
this.logger.warn('Failed to fetch project measures:', errorMessage);
return {};
}
}
/**
* Fetch security hotspots data
*/
async getSecurityHotspots() {
try {
this.logger.info('Fetching security hotspots...');
const response = await this.api.get('/api/hotspots/search', {
params: {
projectKey: this.config.projectKey,
organization: this.config.organization,
ps: 500, // Page size
status: 'TO_REVIEW',
},
});
const hotspots = response.data.hotspots || [];
const total = response.data.paging?.total || 0;
// Group by priority and category
const byPriority = {};
const byCategory = {};
for (const hotspot of hotspots) {
const priority = hotspot.vulnerabilityProbability || 'UNKNOWN';
const category = hotspot.securityCategory || 'UNKNOWN';
byPriority[priority] = (byPriority[priority] || 0) + 1;
byCategory[category] = (byCategory[category] || 0) + 1;
}
return {
total,
byPriority,
byCategory,
hotspots: hotspots.slice(0, 100), // Limit to first 100 for performance
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
this.logger.warn('Failed to fetch security hotspots:', errorMessage);
return {
total: 0,
byPriority: {},
byCategory: {},
hotspots: [],
};
}
}
/**
* Helper method to format technical debt from minutes to human readable format
*/
formatTechnicalDebt(minutes) {
if (minutes === 0)
return '0min';
const days = Math.floor(minutes / (8 * 60)); // 8 hours per day
const hours = Math.floor((minutes % (8 * 60)) / 60);
const mins = minutes % 60;
const parts = [];
if (days > 0)
parts.push(`${days}d`);
if (hours > 0)
parts.push(`${hours}h`);
if (mins > 0 && days === 0)
parts.push(`${mins}min`);
return parts.join(' ') || '0min';
}
/**
* Helper method to format numeric rating to letter rating
*/
formatRating(value) {
if (!value)
return 'N/A';
const numValue = parseInt(value, 10);
const ratings = ['A', 'B', 'C', 'D', 'E'];
return ratings[numValue - 1] || 'N/A';
}
}
exports.SonarQubeService = SonarQubeService;
//# sourceMappingURL=sonarqube.js.map