import type { IssuesParams, SonarQubeIssuesResult, SonarQubeIssue, SonarQubeRule, SonarQubeIssueComment, MarkIssueFalsePositiveParams, MarkIssueWontFixParams, BulkIssueMarkParams, AddCommentToIssueParams, AssignIssueParams, ConfirmIssueParams, UnconfirmIssueParams, ResolveIssueParams, ReopenIssueParams, DoTransitionResponse, } from '../types/index.js'; import { BaseDomain } from './base.js'; // Type aliases for sonarqube-web-api-client enums (not exported by the library) type OwaspTop10Category = 'a1' | 'a2' | 'a3' | 'a4' | 'a5' | 'a6' | 'a7' | 'a8' | 'a9' | 'a10'; type OwaspTop10v2021Category = 'a1' | 'a2' | 'a3' | 'a4' | 'a5' | 'a6' | 'a7' | 'a8' | 'a9' | 'a10'; type SansTop25Category = 'insecure-interaction' | 'risky-resource' | 'porous-defenses'; type IssueFacet = | 'severities' | 'statuses' | 'resolutions' | 'rules' | 'tags' | 'types' | 'author' | 'authors' | 'assignees' | 'assigned_to_me' | 'languages' | 'projects' | 'directories' | 'files' | 'cwe' | 'createdAt' | 'owaspTop10' | 'owaspTop10-2021' | 'owaspAsvs-4.0' | 'owaspMobileTop10-2024' | 'pciDss-3.2' | 'pciDss-4.0' | 'sansTop25' | 'sonarsourceSecurity' | 'stig-ASD_V5R3' | 'casa' | 'codeVariants' | 'cleanCodeAttributeCategories' | 'impactSeverities' | 'impactSoftwareQualities' | 'issueStatuses' | 'prioritizedRule' | 'scopes'; /** * Domain module for issues-related operations */ export class IssuesDomain extends BaseDomain { /** * Gets issues for a project in SonarQube * @param params Parameters including project key, severity, pagination and organization * @returns Promise with the list of issues */ async getIssues(params: IssuesParams): Promise { const { page, pageSize } = params; const builder = this.webApiClient.issues.search(); // Apply all filters using helper methods this.applyComponentFilters(builder, params); this.applyIssueFilters(builder, params); this.applyDateAndAssignmentFilters(builder, params); this.applySecurityAndMetadataFilters(builder, params); // Add pagination if (page !== undefined) { builder.page(page); } if (pageSize !== undefined) { builder.pageSize(pageSize); } const response = await builder.execute(); // Transform to our interface return { issues: response.issues as SonarQubeIssue[], components: (response.components ?? []).map((comp) => ({ key: comp.key, name: comp.name, qualifier: comp.qualifier, enabled: comp.enabled, longName: comp.longName, path: comp.path, })), rules: (response.rules ?? []) as SonarQubeRule[], users: response.users, facets: response.facets, paging: response.paging ?? { pageIndex: 1, pageSize: 100, total: 0 }, }; } /** * Apply component-related filters to the issues search builder * @param builder The search builder * @param params The issues parameters */ private applyComponentFilters( builder: ReturnType, params: IssuesParams ): void { // Component filters if (params.projectKey) { builder.withProjects([params.projectKey]); } if (params.projects) { builder.withProjects(params.projects); } if (params.componentKeys) { builder.withComponents(params.componentKeys); } if (params.components) { builder.withComponents(params.components); } if (params.onComponentOnly) { builder.onComponentOnly(); } if (params.directories) { builder.withDirectories(params.directories); } if (params.files) { builder.withFiles(params.files); } if (params.scopes) { builder.withScopes(params.scopes); } // Branch and PR if (params.branch) { builder.onBranch(params.branch); } if (params.pullRequest) { builder.onPullRequest(params.pullRequest); } } /** * Apply issue-related filters to the search builder * @param builder The search builder * @param params The issues parameters */ private applyIssueFilters( builder: ReturnType, params: IssuesParams ): void { // Issue filters if (params.issues) { builder.withIssues(params.issues); } if (params.severities) { builder.withSeverities(params.severities); } if (params.statuses) { builder.withStatuses(params.statuses); } if (params.resolutions) { builder.withResolutions(params.resolutions); } if (params.resolved !== undefined) { if (params.resolved) { builder.onlyResolved(); } else { builder.onlyUnresolved(); } } if (params.types) { builder.withTypes(params.types); } // Clean Code taxonomy if (params.cleanCodeAttributeCategories) { builder.withCleanCodeAttributeCategories(params.cleanCodeAttributeCategories); } if (params.impactSeverities) { builder.withImpactSeverities(params.impactSeverities); } if (params.impactSoftwareQualities) { builder.withImpactSoftwareQualities(params.impactSoftwareQualities); } if (params.issueStatuses) { builder.withIssueStatuses(params.issueStatuses); } // Rules and tags if (params.rules) { builder.withRules(params.rules); } if (params.tags) { builder.withTags(params.tags); } } /** * Apply date and assignment filters to the search builder * @param builder The search builder * @param params The issues parameters */ private applyDateAndAssignmentFilters( builder: ReturnType, params: IssuesParams ): void { // Date filters if (params.createdAfter) { builder.createdAfter(params.createdAfter); } if (params.createdBefore) { builder.createdBefore(params.createdBefore); } if (params.createdAt) { builder.createdAt(params.createdAt); } if (params.createdInLast) { builder.createdInLast(params.createdInLast); } // Assignment if (params.assigned !== undefined) { if (params.assigned) { builder.onlyAssigned(); } else { builder.onlyUnassigned(); } } if (params.assignees) { builder.assignedToAny(params.assignees); } if (params.author) { builder.byAuthor(params.author); } if (params.authors) { builder.byAuthors(params.authors); } } /** * Apply security standards and metadata filters to the search builder * @param builder The search builder * @param params The issues parameters */ private applySecurityAndMetadataFilters( builder: ReturnType, params: IssuesParams ): void { // Security standards if (params.cwe) { builder.withCwe(params.cwe); } if (params.owaspTop10) { builder.withOwaspTop10(params.owaspTop10 as OwaspTop10Category[]); } if (params.owaspTop10v2021) { builder.withOwaspTop10v2021(params.owaspTop10v2021 as OwaspTop10v2021Category[]); } if (params.sansTop25) { // NOTE: withSansTop25 is deprecated since SonarQube 10.0, but kept for backward compatibility builder.withSansTop25(params.sansTop25 as SansTop25Category[]); } if (params.sonarsourceSecurity) { builder.withSonarSourceSecurity(params.sonarsourceSecurity); } if (params.sonarsourceSecurityCategory) { builder.withSonarSourceSecurityNew(params.sonarsourceSecurityCategory); } // Languages if (params.languages) { builder.withLanguages(params.languages); } // Facets if (params.facets) { builder.withFacets(params.facets as IssueFacet[]); } if (params.facetMode) { builder.withFacetMode(params.facetMode); } // New code if (params.sinceLeakPeriod) { builder.sinceLeakPeriod(); } if (params.inNewCodePeriod) { builder.inNewCodePeriod(); } // Sorting if (params.s) { builder.sortBy(params.s, params.asc); } // Additional fields if (params.additionalFields) { builder.withAdditionalFields(params.additionalFields); } // Deprecated parameters // Note: hotspots parameter is deprecated and not supported by the current API if (params.severity) { builder.withSeverities([params.severity]); } } /** * Mark an issue as false positive * @param params Parameters including issue key and optional comment * @returns Promise with the updated issue and related data */ async markIssueFalsePositive( params: MarkIssueFalsePositiveParams ): Promise { const request = { issue: params.issueKey, transition: 'falsepositive' as const, }; // Add comment if provided (using separate API call if needed) if (params.comment) { // First add the comment, then perform the transition await this.webApiClient.issues.addComment({ issue: params.issueKey, text: params.comment, }); } return this.webApiClient.issues.doTransition(request); } /** * Mark an issue as won't fix * @param params Parameters including issue key and optional comment * @returns Promise with the updated issue and related data */ async markIssueWontFix(params: MarkIssueWontFixParams): Promise { const request = { issue: params.issueKey, transition: 'wontfix' as const, }; // Add comment if provided (using separate API call if needed) if (params.comment) { // First add the comment, then perform the transition await this.webApiClient.issues.addComment({ issue: params.issueKey, text: params.comment, }); } return this.webApiClient.issues.doTransition(request); } /** * Mark multiple issues as false positive * @param params Parameters including issue keys and optional comment * @returns Promise with array of updated issues and related data */ async markIssuesFalsePositive(params: BulkIssueMarkParams): Promise { return Promise.all( params.issueKeys.map((issueKey) => { const requestParams: MarkIssueFalsePositiveParams = { issueKey, ...(params.comment && { comment: params.comment }), }; return this.markIssueFalsePositive(requestParams); }) ); } /** * Mark multiple issues as won't fix * @param params Parameters including issue keys and optional comment * @returns Promise with array of updated issues and related data */ async markIssuesWontFix(params: BulkIssueMarkParams): Promise { const results: DoTransitionResponse[] = []; for (const issueKey of params.issueKeys) { const requestParams: MarkIssueWontFixParams = { issueKey, ...(params.comment && { comment: params.comment }), }; const result = await this.markIssueWontFix(requestParams); results.push(result); } return results; } /** * Add a comment to an issue * @param params Parameters including issue key and comment text * @returns Promise with the created comment details */ async addCommentToIssue(params: AddCommentToIssueParams): Promise { const response = await this.webApiClient.issues.addComment({ issue: params.issueKey, text: params.text, }); // The API returns the full issue with comments, so we need to extract the latest comment const issue = response.issue as SonarQubeIssue; const comments = issue.comments || []; // Sort comments by timestamp to ensure chronological order comments.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()); // The newly added comment should now be the last one const newComment = comments.at(-1); if (!newComment) { throw new Error('Failed to retrieve the newly added comment'); } return newComment; } /** * Assign an issue to a user * @param params Assignment parameters * @returns The updated issue details */ async assignIssue(params: AssignIssueParams): Promise { // Call the assign API const assignRequest: { issue: string; assignee?: string; } = { issue: params.issueKey, ...(params.assignee && { assignee: params.assignee }), }; await this.webApiClient.issues.assign(assignRequest); // Fetch and return the updated issue using the same search as getIssues const searchBuilder = this.webApiClient.issues.search(); searchBuilder.withIssues([params.issueKey]); searchBuilder.withAdditionalFields(['_all']); const response = await searchBuilder.execute(); if (!response.issues || response.issues.length === 0) { throw new Error(`Issue ${params.issueKey} not found after assignment`); } return response.issues[0] as SonarQubeIssue; } /** * Confirm an issue * @param params Parameters including issue key and optional comment * @returns Promise with the updated issue and related data */ async confirmIssue(params: ConfirmIssueParams): Promise { const request = { issue: params.issueKey, transition: 'confirm' as const, }; if (params.comment) { await this.webApiClient.issues.addComment({ issue: params.issueKey, text: params.comment, }); } return this.webApiClient.issues.doTransition(request); } /** * Unconfirm an issue * @param params Parameters including issue key and optional comment * @returns Promise with the updated issue and related data */ async unconfirmIssue(params: UnconfirmIssueParams): Promise { const request = { issue: params.issueKey, transition: 'unconfirm' as const, }; if (params.comment) { await this.webApiClient.issues.addComment({ issue: params.issueKey, text: params.comment, }); } return this.webApiClient.issues.doTransition(request); } /** * Resolve an issue * @param params Parameters including issue key and optional comment * @returns Promise with the updated issue and related data */ async resolveIssue(params: ResolveIssueParams): Promise { const request = { issue: params.issueKey, transition: 'resolve' as const, }; if (params.comment) { await this.webApiClient.issues.addComment({ issue: params.issueKey, text: params.comment, }); } return this.webApiClient.issues.doTransition(request); } /** * Reopen an issue * @param params Parameters including issue key and optional comment * @returns Promise with the updated issue and related data */ async reopenIssue(params: ReopenIssueParams): Promise { const request = { issue: params.issueKey, transition: 'reopen' as const, }; if (params.comment) { await this.webApiClient.issues.addComment({ issue: params.issueKey, text: params.comment, }); } return this.webApiClient.issues.doTransition(request); } }