Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/pipelines/sq-10.0/protobuf/build-issues/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import logger from '../../../../shared/utils/logger.js';
import { isExternalIssue } from '../build-external-issues.js';
import { isClosedOrFixed } from '../../../../shared/utils/issue-filters/is-closed-or-fixed.js';
import { buildIssueMessage } from './helpers/build-issue-message.js';

export function buildIssues(builder) {
Expand All @@ -12,6 +13,7 @@ export function buildIssues(builder) {
let skippedIssues = 0;

builder.data.issues.forEach(issue => {
if (isClosedOrFixed(issue)) return;
if (isExternalIssue(issue, sonarCloudRepos)) return;

if (!builder.validComponentKeys?.has(issue.component)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logger from '../../../../../shared/utils/logger.js';
import { mapConcurrent, createProgressLogger } from '../../../../../shared/utils/concurrency.js';
import { isClosedOrFixed } from '../../../../../shared/utils/issue-filters/is-closed-or-fixed.js';
import { applyManualChangesPreFilter } from '../../../../../shared/utils/issue-sync/apply-pre-filter.js';
import { waitForScIndexing } from '../../../../../shared/utils/issue-sync/wait-for-sc-indexing.js';
import { matchIssues } from './helpers/match-issues.js';
Expand All @@ -19,11 +20,15 @@ export async function syncIssues(projectKey, sqIssues, client, options = {}) {
const userMappings = options.userMappings || null;
const stats = createEmptyStats();

let issuesToSync = sqIssues;
const beforeFilter = sqIssues.length;
let issuesToSync = sqIssues.filter(i => !isClosedOrFixed(i));
if (issuesToSync.length < beforeFilter) {
logger.info(`Filtered out ${beforeFilter - issuesToSync.length} closed/fixed issues`);
}
let changelogMap = new Map();

if (sqClient) {
({ issuesToSync, changelogMap } = await applyManualChangesPreFilter(sqIssues, sqClient, stats, concurrency));
({ issuesToSync, changelogMap } = await applyManualChangesPreFilter(issuesToSync, sqClient, stats, concurrency));
if (issuesToSync.length === 0) { logSyncSummary(stats); return stats; }
}

Expand Down
5 changes: 4 additions & 1 deletion src/pipelines/sq-10.0/sonarqube/api/issues-hotspots.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import { fetchWithSlicing } from '../../../../shared/utils/search-slicer/index.j

// All issue statuses — pre-10.4 lifecycle + 10.4+ lifecycle.
// The SonarQube API ignores unknown values, so including both sets is safe.
const ALL_STATUSES = 'OPEN,CONFIRMED,REOPENED,RESOLVED,CLOSED,FALSE_POSITIVE,ACCEPTED,FIXED';
// CLOSED and FIXED are excluded — resolved issues should not be recreated on the destination.
// RESOLVED is kept because it includes FALSE-POSITIVE and WONTFIX resolutions
// that should be migrated (downstream filter handles resolution=FIXED).
const ALL_STATUSES = 'OPEN,CONFIRMED,REOPENED,RESOLVED,FALSE_POSITIVE,ACCEPTED';

export async function getIssues(probeTotal, getPaginated, projectKey, filters = {}) {
logger.info(`Fetching issues for project: ${projectKey}`);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logger from '../../../../../shared/utils/logger.js';
import { isExternalIssue } from '../../build-external-issues.js';
import { isClosedOrFixed } from '../../../../../shared/utils/issue-filters/is-closed-or-fixed.js';
import { buildIssueMessage } from './build-issue-message.js';

// -------- Main Logic --------
Expand All @@ -13,6 +14,7 @@ export function buildIssues(builder) {
let skippedIssues = 0;

builder.data.issues.forEach(issue => {
if (isClosedOrFixed(issue)) return;
if (isExternalIssue(issue, sonarCloudRepos)) return;

if (!builder.validComponentKeys?.has(issue.component)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { syncSingleIssue } from './sync-single-issue.js';
import { createSyncStats } from './create-sync-stats.js';
import { logSyncStats } from './log-sync-stats.js';
import { mapConcurrent, createProgressLogger } from '../../../../../../shared/utils/concurrency.js';
import { isClosedOrFixed } from '../../../../../../shared/utils/issue-filters/is-closed-or-fixed.js';
import { applyManualChangesPreFilter } from '../../../../../../shared/utils/issue-sync/apply-pre-filter.js';
import { waitForScIndexing } from '../../../../../../shared/utils/issue-sync/wait-for-sc-indexing.js';
import logger from '../../../../../../shared/utils/logger.js';
Expand All @@ -19,11 +20,15 @@ export async function syncIssues(projectKey, sqIssues, client, options = {}) {
const userMappings = options.userMappings || null;
const stats = createSyncStats();

let issuesToSync = sqIssues;
const beforeFilter = sqIssues.length;
let issuesToSync = sqIssues.filter(i => !isClosedOrFixed(i));
if (issuesToSync.length < beforeFilter) {
logger.info(`Filtered out ${beforeFilter - issuesToSync.length} closed/fixed issues`);
}
let changelogMap = new Map();

if (sqClient) {
({ issuesToSync, changelogMap } = await applyManualChangesPreFilter(sqIssues, sqClient, stats, concurrency));
({ issuesToSync, changelogMap } = await applyManualChangesPreFilter(issuesToSync, sqClient, stats, concurrency));
if (issuesToSync.length === 0) { logSyncStats(stats); return stats; }
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ import { buildMeasureMethods } from './measure-methods.js';
import { buildSourceMethods } from './source-methods.js';
import { buildConnectionMethods } from './connection-methods.js';
import { buildDelegateMethods } from './delegate-methods.js';
import logger from '../../../../../shared/utils/logger.js';

// -------- Main Logic --------

// Factory function to create a SonarQube API client.
export function createSonarQubeClient(config) {
const baseURL = config.url.replace(/\/$/, '');
const client = createHttpClient(baseURL, config.token);
logger.info(`OKO Created SonarQube client for ${baseURL} with token: ${client.token}`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debug log leaks authentication token to logs

High Severity

An accidentally committed debug statement logs the SonarQube authentication token in plaintext via logger.info. The "OKO" prefix is a clear debug marker. This exposes sensitive credentials in application logs, which could end up in log aggregation systems, CI output, or shared log files.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 397bab4. Configure here.

const projectKey = config.projectKey;

const gp = (endpoint, params, dataKey) => getPaginated(client, endpoint, params, dataKey);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export function handleApiError(error, baseURL) {
if (error.response) {
const { status, data, config } = error.response;
if (status === 401 || status === 403) {
throw new AuthenticationError(`Authentication failed for SonarQube: ${data.errors?.[0]?.msg || 'Invalid credentials'}`, 'SonarQube');
throw new AuthenticationError(`Authentication failed for SonarQube: DATA = ${data} - ${data.errors?.[0]?.msg || 'Invalid credentials I believe'}`, 'SonarQube');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debug text in authentication error message

Medium Severity

The authentication error message was changed to include debug artifacts: raw DATA = ${data} dump and the informal fallback string 'Invalid credentials I believe'. This produces unprofessional, potentially confusing error messages for users and may leak raw response data.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 397bab4. Configure here.

}
const message = data.errors?.[0]?.msg || data.message || error.message;
throw new SonarQubeAPIError(`SonarQube API error (${status}): ${message}`, status, config.url);
Expand Down
4 changes: 2 additions & 2 deletions src/pipelines/sq-10.4/sonarqube/api/issues-hotspots.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import logger from '../../../../shared/utils/logger.js';
import { fetchWithSlicing } from '../../../../shared/utils/search-slicer/index.js';

// 10.4+ uses the `issueStatuses` parameter with the modern lifecycle values.
// Include CLOSED so that closed issues are also extracted for migration.
const ISSUE_STATUSES = 'OPEN,CONFIRMED,FALSE_POSITIVE,ACCEPTED,FIXED,CLOSED';
// FIXED and CLOSED are excluded — resolved issues should not be recreated on the destination.
const ISSUE_STATUSES = 'OPEN,CONFIRMED,FALSE_POSITIVE,ACCEPTED';

export async function getIssues(probeTotal, getPaginated, projectKey, filters = {}) {
logger.info(`Fetching issues for project: ${projectKey}`);
Expand Down
2 changes: 2 additions & 0 deletions src/pipelines/sq-2025/protobuf/build-issues/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logger from '../../../../shared/utils/logger.js';
import { isExternalIssue } from '../build-external-issues.js';
import { isClosedOrFixed } from '../../../../shared/utils/issue-filters/is-closed-or-fixed.js';
import { buildSingleIssue } from './helpers/build-single-issue.js';

// -------- Build Issues --------
Expand All @@ -12,6 +13,7 @@ export function buildIssues(builder) {
let skippedIssues = 0;

builder.data.issues.forEach(issue => {
if (isClosedOrFixed(issue)) return;
if (isExternalIssue(issue, sonarCloudRepos)) return;
if (!builder.validComponentKeys?.has(issue.component)) { skippedIssues++; return; }

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import logger from '../../../../../shared/utils/logger.js';
import { mapConcurrent, createProgressLogger } from '../../../../../shared/utils/concurrency.js';
import { parallelSyncIssues } from '../../../../../shared/utils/concurrency/helpers/parallel-issue-sync.js';
import { isClosedOrFixed } from '../../../../../shared/utils/issue-filters/is-closed-or-fixed.js';
import { applyManualChangesPreFilter } from '../../../../../shared/utils/issue-sync/apply-pre-filter.js';
import { waitForScIndexing } from '../../../../../shared/utils/issue-sync/wait-for-sc-indexing.js';
import { matchIssues } from './helpers/match-issues.js';
Expand All @@ -24,11 +25,15 @@ export async function syncIssues(projectKey, sqIssues, client, options = {}) {
const userMappings = options.userMappings || null;
const stats = createSyncStats();

let issuesToSync = sqIssues;
const beforeFilter = sqIssues.length;
let issuesToSync = sqIssues.filter(i => !isClosedOrFixed(i));
if (issuesToSync.length < beforeFilter) {
logger.info(`Filtered out ${beforeFilter - issuesToSync.length} closed/fixed issues`);
}
let changelogMap = new Map();

if (sqClient) {
({ issuesToSync, changelogMap } = await applyManualChangesPreFilter(sqIssues, sqClient, stats, concurrency));
({ issuesToSync, changelogMap } = await applyManualChangesPreFilter(issuesToSync, sqClient, stats, concurrency));
if (issuesToSync.length === 0) { logSyncSummary(stats); return stats; }
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { probeTotal } from './probe-total.js';

// -------- Issue Methods --------

const ISSUE_STATUSES = 'OPEN,CONFIRMED,FALSE_POSITIVE,ACCEPTED,FIXED,IN_SANDBOX';
// FIXED is excluded — resolved issues should not be recreated on the destination.
const ISSUE_STATUSES = 'OPEN,CONFIRMED,FALSE_POSITIVE,ACCEPTED,IN_SANDBOX';

/** Attach issue and duplication methods to the client instance. */
export function attachIssueMethods(inst) {
Expand Down
3 changes: 2 additions & 1 deletion src/pipelines/sq-2025/sonarqube/api/issues-hotspots.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import logger from '../../../../shared/utils/logger.js';
import { fetchWithSlicing } from '../../../../shared/utils/search-slicer/index.js';

// 2025.x uses the `issueStatuses` parameter with the modern lifecycle values.
const ISSUE_STATUSES = 'OPEN,CONFIRMED,FALSE_POSITIVE,ACCEPTED,FIXED,IN_SANDBOX';
// FIXED is excluded — resolved issues should not be recreated on the destination.
const ISSUE_STATUSES = 'OPEN,CONFIRMED,FALSE_POSITIVE,ACCEPTED,IN_SANDBOX';

export async function getIssues(probeTotal, getPaginated, projectKey, filters = {}) {
logger.info(`Fetching issues for project: ${projectKey}`);
Expand Down
2 changes: 2 additions & 0 deletions src/pipelines/sq-9.9/protobuf/build-issues/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logger from '../../../../shared/utils/logger.js';
import { isExternalIssue } from '../build-external-issues.js';
import { isClosedOrFixed } from '../../../../shared/utils/issue-filters/is-closed-or-fixed.js';

// -------- Build Issue Messages --------

Expand All @@ -10,6 +11,7 @@ export function buildIssues(builder) {
let skippedIssues = 0;

builder.data.issues.forEach(issue => {
if (isClosedOrFixed(issue)) return;
if (isExternalIssue(issue, sonarCloudRepos)) return;
if (!builder.validComponentKeys?.has(issue.component)) { skippedIssues++; return; }
const componentRef = builder.componentRefMap.get(issue.component);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logger from '../../../../../../shared/utils/logger.js';
import { mapConcurrent, createProgressLogger } from '../../../../../../shared/utils/concurrency.js';
import { isClosedOrFixed } from '../../../../../../shared/utils/issue-filters/is-closed-or-fixed.js';
import { applyManualChangesPreFilter } from '../../../../../../shared/utils/issue-sync/apply-pre-filter.js';
import { waitForScIndexing } from '../../../../../../shared/utils/issue-sync/wait-for-sc-indexing.js';
import { matchIssues } from './match-issues.js';
Expand All @@ -17,11 +18,15 @@ export async function syncIssues(projectKey, sqIssues, client, options = {}) {
const userMappings = options.userMappings || null;
const stats = createEmptyStats();

let issuesToSync = sqIssues;
const beforeFilter = sqIssues.length;
let issuesToSync = sqIssues.filter(i => !isClosedOrFixed(i));
if (issuesToSync.length < beforeFilter) {
logger.info(`Filtered out ${beforeFilter - issuesToSync.length} closed/fixed issues`);
}
let changelogMap = new Map();

if (sqClient) {
({ issuesToSync, changelogMap } = await applyManualChangesPreFilter(sqIssues, sqClient, stats, concurrency));
({ issuesToSync, changelogMap } = await applyManualChangesPreFilter(issuesToSync, sqClient, stats, concurrency));
if (issuesToSync.length === 0) { logSyncStats(stats); return stats; }
}

Expand Down
5 changes: 4 additions & 1 deletion src/pipelines/sq-9.9/sonarqube/api/issues-hotspots.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import { fetchWithSlicing } from '../../../../shared/utils/search-slicer/index.j

// SonarQube 9.9 only supports the classic issue lifecycle statuses.
// FALSE_POSITIVE, ACCEPTED, FIXED are NOT valid in 9.9 (causes 400 error).
const ALL_STATUSES = 'OPEN,CONFIRMED,REOPENED,RESOLVED,CLOSED';
// CLOSED is excluded — resolved issues should not be recreated on the destination.
// RESOLVED is kept because it includes FALSE-POSITIVE and WONTFIX resolutions
// that should be migrated (downstream filter handles resolution=FIXED).
const ALL_STATUSES = 'OPEN,CONFIRMED,REOPENED,RESOLVED';

export async function getIssues(probeTotal, getPaginated, projectKey, filters = {}) {
logger.info(`Fetching issues for project: ${projectKey}`);
Expand Down
11 changes: 11 additions & 0 deletions src/shared/utils/issue-filters/is-closed-or-fixed.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* Returns true if the issue is closed or fixed and should be excluded from migration.
* Covers all lifecycle variants:
* - Modern lifecycle: explicit CLOSED or FIXED status
* - Classic lifecycle: RESOLVED status with resolution=FIXED
*/
export function isClosedOrFixed(issue) {
if (issue.status === 'CLOSED' || issue.status === 'FIXED') return true;
if (issue.status === 'RESOLVED' && issue.resolution === 'FIXED') return true;
return false;
}
6 changes: 3 additions & 3 deletions test/sonarcloud/migrators/migrators.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1696,7 +1696,7 @@ test('syncIssues handles RESOLVED status', async t => {
t.is(client.transitionIssue.firstCall.args[1], 'resolve');
});

test('syncIssues handles CLOSED status', async t => {
test('syncIssues filters out CLOSED issues (no transition propagation)', async t => {
const client = mockClient({
searchIssues: sinon.stub().resolves([
{ key: 'sc-i1', rule: 'js:S1001', component: 'proj:src/a.js', line: 5, status: 'OPEN' }
Expand All @@ -1715,8 +1715,8 @@ test('syncIssues handles CLOSED status', async t => {

const stats = await syncIssues('proj', sqIssues, client, { concurrency: 1 });

t.is(stats.transitioned, 1);
t.is(client.transitionIssue.firstCall.args[1], 'resolve');
t.is(stats.transitioned, 0);
t.false(client.transitionIssue.called);
});

test('syncIssues handles ACCEPTED status', async t => {
Expand Down