From 1c1c3fde0b96c7550dcc0adf07f63608da51f24f Mon Sep 17 00:00:00 2001 From: Lorenz van Herwaarden Date: Fri, 12 Sep 2025 17:49:08 +0200 Subject: [PATCH 01/10] Add security finding external issue link create mutation This adds a mutation to allow creating an external issue link for a security finding. It finds the or creates a related vulnerability and then creates the external issue link. --- doc/api/graphql/reference/_index.md | 22 ++ .../pipeline/vulnerability_finding_modal.vue | 26 ++- ...finding_create_jira_issue.mutation.graphql | 13 ++ ...ability_create_jira_issue.mutation.graphql | 6 +- .../ee/projects/merge_requests_controller.rb | 1 + ee/app/graphql/ee/types/mutation_type.rb | 1 + .../finding/create_external_issue_link.rb | 50 +++++ .../create_external_issue_link_service.rb | 50 +++++ .../finding_create_jira_issue_mutation.yml | 10 + .../components/pipeline/mock_data.js | 16 +- .../vulnerability_finding_modal_spec.js | 53 ++++- .../create_external_issue_link_spec.rb | 80 +++++++ ...create_external_issue_link_service_spec.rb | 199 ++++++++++++++++++ 13 files changed, 512 insertions(+), 15 deletions(-) create mode 100644 ee/app/assets/javascripts/security_dashboard/graphql/mutations/finding_create_jira_issue.mutation.graphql create mode 100644 ee/app/graphql/mutations/security/finding/create_external_issue_link.rb create mode 100644 ee/app/services/security/findings/create_external_issue_link_service.rb create mode 100644 ee/config/feature_flags/wip/finding_create_jira_issue_mutation.yml create mode 100644 ee/spec/graphql/mutations/security/finding/create_external_issue_link_spec.rb create mode 100644 ee/spec/services/security/findings/create_external_issue_link_service_spec.rb diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md index d5cb62319c087c..88542bdf967e62 100644 --- a/doc/api/graphql/reference/_index.md +++ b/doc/api/graphql/reference/_index.md @@ -11785,6 +11785,28 @@ Input type: `SecurityFindingDismissInput` | `securityFinding` | [`PipelineSecurityReportFinding`](#pipelinesecurityreportfinding) | Dismissed finding. | | `uuid` | [`String`](#string) | UUID of dismissed finding. | +### `Mutation.securityFindingExternalIssueLinkCreate` + +Input type: `SecurityFindingExternalIssueLinkCreateInput` + +#### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| `externalTracker` | [`VulnerabilityExternalIssueLinkExternalTracker!`](#vulnerabilityexternalissuelinkexternaltracker) | External tracker type of the external issue link. | +| `linkType` | [`VulnerabilityExternalIssueLinkType!`](#vulnerabilityexternalissuelinktype) | Type of the external issue link. | +| `project` | [`ProjectID!`](#projectid) | ID of the project to attach the issue to. | +| `uuid` | [`String!`](#string) | UUID of the security finding to be used to create an issue. | + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| `errors` | [`[String!]!`](#string) | Errors encountered during the mutation. | +| `externalIssueLink` | [`VulnerabilityExternalIssueLink`](#vulnerabilityexternalissuelink) | Created external issue link. | + ### `Mutation.securityFindingRevertToDetected` Input type: `SecurityFindingRevertToDetectedInput` diff --git a/ee/app/assets/javascripts/security_dashboard/components/pipeline/vulnerability_finding_modal.vue b/ee/app/assets/javascripts/security_dashboard/components/pipeline/vulnerability_finding_modal.vue index 0be4477c983cea..5a239580a47837 100644 --- a/ee/app/assets/javascripts/security_dashboard/components/pipeline/vulnerability_finding_modal.vue +++ b/ee/app/assets/javascripts/security_dashboard/components/pipeline/vulnerability_finding_modal.vue @@ -15,6 +15,7 @@ import { __, s__ } from '~/locale'; import download from '~/lib/utils/downloader'; import { visitUrl } from '~/lib/utils/url_utility'; import { fetchPolicies } from '~/lib/graphql'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { CREATE_MR_ACTION, DOWNLOAD_PATCH_ACTION, @@ -29,7 +30,8 @@ import dismissFindingMutation from 'ee/security_dashboard/graphql/mutations/dism import createMergeRequestMutation from 'ee/security_dashboard/graphql/mutations/finding_create_merge_request.mutation.graphql'; import revertFindingToDetectedMutation from 'ee/security_dashboard/graphql/mutations/revert_finding_to_detected.mutation.graphql'; import createIssueMutation from 'ee/security_dashboard/graphql/mutations/finding_create_issue.mutation.graphql'; -import createJiraIssueMutation from 'ee/security_dashboard/graphql/mutations/vulnerability_create_jira_issue.mutation.graphql'; +import vulnerabilityCreateJiraIssueMutation from 'ee/security_dashboard/graphql/mutations/vulnerability_create_jira_issue.mutation.graphql'; +import findingCreateJiraIssueMutation from 'ee/security_dashboard/graphql/mutations/finding_create_jira_issue.mutation.graphql'; import VulnerabilityCodeFlow from 'ee/vue_shared/components/code_flow/vulnerability_code_flow.vue'; import glAbilitiesMixin from '~/vue_shared/mixins/gl_abilities_mixin'; import { VULNERABILITY_DETAIL_CODE_FLOWS } from 'ee/security_dashboard/constants'; @@ -75,7 +77,7 @@ export default { GlTab, ResolveWithAiButton, }, - mixins: [glAbilitiesMixin()], + mixins: [glAbilitiesMixin(), glFeatureFlagMixin()], inject: { vulnerabilitiesQuery: { default: null, @@ -483,18 +485,26 @@ export default { async createJiraIssue() { const { vulnerabilityId } = this; - if (!vulnerabilityId) { - return; + const useFindingMutation = this.glFeatures.findingCreateJiraIssueMutation; + + if (!useFindingMutation) { + if (!vulnerabilityId) { + return; + } } const { createJiraIssueErrorMessage } = this.$options.i18n; this.isCreatingJiraIssue = true; + const variables = useFindingMutation + ? { findingUuid: this.findingUuid, projectId: this.projectId } + : { vulnerabilityId }; + const data = await this.executeMutation({ - mutation: createJiraIssueMutation, - variables: { - vulnerabilityId, - }, + mutation: useFindingMutation + ? findingCreateJiraIssueMutation + : vulnerabilityCreateJiraIssueMutation, + variables, errorMessage: createJiraIssueErrorMessage, }); diff --git a/ee/app/assets/javascripts/security_dashboard/graphql/mutations/finding_create_jira_issue.mutation.graphql b/ee/app/assets/javascripts/security_dashboard/graphql/mutations/finding_create_jira_issue.mutation.graphql new file mode 100644 index 00000000000000..4887ba2b2091f4 --- /dev/null +++ b/ee/app/assets/javascripts/security_dashboard/graphql/mutations/finding_create_jira_issue.mutation.graphql @@ -0,0 +1,13 @@ +mutation securityFindingJiraIssueCreate($findingUuid: String!, $projectId: ProjectID!) { + securityFindingExternalIssueLinkCreate( + input: { uuid: $findingUuid, project: $projectId, linkType: CREATED, externalTracker: JIRA } + ) { + errors + externalIssueLink { + id + externalIssue { + webUrl + } + } + } +} diff --git a/ee/app/assets/javascripts/security_dashboard/graphql/mutations/vulnerability_create_jira_issue.mutation.graphql b/ee/app/assets/javascripts/security_dashboard/graphql/mutations/vulnerability_create_jira_issue.mutation.graphql index 44641bc07a1b2a..4887ba2b2091f4 100644 --- a/ee/app/assets/javascripts/security_dashboard/graphql/mutations/vulnerability_create_jira_issue.mutation.graphql +++ b/ee/app/assets/javascripts/security_dashboard/graphql/mutations/vulnerability_create_jira_issue.mutation.graphql @@ -1,6 +1,6 @@ -mutation vulnerabilityJiraIssueCreate($vulnerabilityId: VulnerabilityID!) { - vulnerabilityExternalIssueLinkCreate( - input: { id: $vulnerabilityId, linkType: CREATED, externalTracker: JIRA } +mutation securityFindingJiraIssueCreate($findingUuid: String!, $projectId: ProjectID!) { + securityFindingExternalIssueLinkCreate( + input: { uuid: $findingUuid, project: $projectId, linkType: CREATED, externalTracker: JIRA } ) { errors externalIssueLink { diff --git a/ee/app/controllers/ee/projects/merge_requests_controller.rb b/ee/app/controllers/ee/projects/merge_requests_controller.rb index a2680e1a90f4cd..a6c95bc43c5f8f 100644 --- a/ee/app/controllers/ee/projects/merge_requests_controller.rb +++ b/ee/app/controllers/ee/projects/merge_requests_controller.rb @@ -16,6 +16,7 @@ module MergeRequestsController push_frontend_ability(ability: :resolve_vulnerability_with_ai, resource: @project, user: current_user) push_frontend_ability(ability: :measure_comment_temperature, resource: merge_request, user: current_user) push_frontend_feature_flag(:ai_duo_agent_fix_pipeline_button, project) + push_frontend_feature_flag(:finding_create_jira_issue_mutation, @project) end before_action do diff --git a/ee/app/graphql/ee/types/mutation_type.rb b/ee/app/graphql/ee/types/mutation_type.rb index 455fef4c76357e..6581654c5e3a05 100644 --- a/ee/app/graphql/ee/types/mutation_type.rb +++ b/ee/app/graphql/ee/types/mutation_type.rb @@ -99,6 +99,7 @@ def self.authorization_scopes mount_mutation ::Mutations::SecretsManagement::Permissions::Update mount_mutation ::Mutations::SecretsManagement::Permissions::Delete mount_mutation ::Mutations::Security::Finding::CreateIssue + mount_mutation ::Mutations::Security::Finding::CreateExternalIssueLink mount_mutation ::Mutations::Security::Finding::CreateMergeRequest mount_mutation ::Mutations::Security::Finding::CreateVulnerability, experiment: { milestone: '17.5' } mount_mutation ::Mutations::Security::Finding::Dismiss diff --git a/ee/app/graphql/mutations/security/finding/create_external_issue_link.rb b/ee/app/graphql/mutations/security/finding/create_external_issue_link.rb new file mode 100644 index 00000000000000..3c8d1d9a2f42b2 --- /dev/null +++ b/ee/app/graphql/mutations/security/finding/create_external_issue_link.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module Mutations + module Security + module Finding + class CreateExternalIssueLink < BaseMutation + graphql_name 'SecurityFindingExternalIssueLinkCreate' + + authorize :read_security_resource + + field :external_issue_link, Types::Vulnerability::ExternalIssueLinkType, + null: true, + description: 'Created external issue link.' + + argument :uuid, + GraphQL::Types::String, + required: true, + description: 'UUID of the security finding to be used to create an issue.' + + argument :link_type, + ::Types::Vulnerability::ExternalIssueLinkTypeEnum, + required: true, + description: 'Type of the external issue link.' + + argument :project, ::Types::GlobalIDType[::Project], + required: true, + description: 'ID of the project to attach the issue to.' + + argument :external_tracker, + ::Types::Vulnerability::ExternalIssueLinkExternalTrackerEnum, + required: true, + description: 'External tracker type of the external issue link.' + + def resolve(uuid:, project:, link_type:, external_tracker:) + project = authorized_find!(id: project) + params = { security_finding_uuid: uuid, link_type: link_type, external_tracker: external_tracker } + + result = ::Security::Findings::CreateExternalIssueLinkService.new(project: project, + current_user: current_user, + params: params).execute + + { + external_issue_link: result.success? ? result.payload[:record] : nil, + errors: result.errors + } + end + end + end + end +end diff --git a/ee/app/services/security/findings/create_external_issue_link_service.rb b/ee/app/services/security/findings/create_external_issue_link_service.rb new file mode 100644 index 00000000000000..d7285264069dfa --- /dev/null +++ b/ee/app/services/security/findings/create_external_issue_link_service.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module Security + module Findings + class CreateExternalIssueLinkService < ::BaseProjectService + attr_reader :error_message + + def execute + vulnerability_response = create_vulnerability + return error(vulnerability_response[:message]) if vulnerability_response.error? + + vulnerability = vulnerability_response.payload[:vulnerability] + + external_issue_link_response = create_external_issue_link(vulnerability) + return error(external_issue_link_response[:message]) if external_issue_link_response.error? + + external_issue_link = external_issue_link_response.payload[:record] + + success(external_issue_link) + end + + private + + def create_vulnerability + Vulnerabilities::FindOrCreateFromSecurityFindingService.new( + project: @project, + current_user: @current_user, + params: params, + present_on_default_branch: false, + state: 'detected').execute + end + + def create_external_issue_link(vulnerability) + ::VulnerabilityExternalIssueLinks::CreateService.new( + @current_user, + vulnerability, + params[:external_tracker], + link_type: params[:link_type]).execute + end + + def error(message) + ServiceResponse.error(message: message) + end + + def success(external_issue_link) + ServiceResponse.success(payload: { record: external_issue_link }) + end + end + end +end diff --git a/ee/config/feature_flags/wip/finding_create_jira_issue_mutation.yml b/ee/config/feature_flags/wip/finding_create_jira_issue_mutation.yml new file mode 100644 index 00000000000000..813661b390f24b --- /dev/null +++ b/ee/config/feature_flags/wip/finding_create_jira_issue_mutation.yml @@ -0,0 +1,10 @@ +--- +name: finding_create_jira_issue_mutation +description: +feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/441954 +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/204889 +rollout_issue_url: +milestone: '18.4' +group: group::security insights +type: wip +default_enabled: false diff --git a/ee/spec/frontend/security_dashboard/components/pipeline/mock_data.js b/ee/spec/frontend/security_dashboard/components/pipeline/mock_data.js index 16cf62936a062e..996b192ce2b617 100644 --- a/ee/spec/frontend/security_dashboard/components/pipeline/mock_data.js +++ b/ee/spec/frontend/security_dashboard/components/pipeline/mock_data.js @@ -700,7 +700,7 @@ export const securityFindingCreateIssueMutationResponse = { }, }; -export const securityFindingCreateExternalIssueMutationResponse = { +export const vulnerabilityCreateExternalIssueMutationResponse = { data: { vulnerabilityExternalIssueLinkCreate: { errors: [], @@ -714,6 +714,20 @@ export const securityFindingCreateExternalIssueMutationResponse = { }, }; +export const securityFindingCreateExternalIssueMutationResponse = { + data: { + securityFindingExternalIssueLinkCreate: { + errors: [], + externalIssueLink: { + id: '1', + externalIssue: { + webUrl: 'https://jira.com/issue/1', + }, + }, + }, + }, +}; + export const getVulnerabilityExternalIssuesQueryResponse = ({ externalIssues = [ { id: '1', externalIssue: { webUrl: 'https://jira.com/1', relativeReference: 'GTL#1' } }, diff --git a/ee/spec/frontend/security_dashboard/components/pipeline/vulnerability_finding_modal_spec.js b/ee/spec/frontend/security_dashboard/components/pipeline/vulnerability_finding_modal_spec.js index fef79b7ffcf646..76bec17032c1be 100644 --- a/ee/spec/frontend/security_dashboard/components/pipeline/vulnerability_finding_modal_spec.js +++ b/ee/spec/frontend/security_dashboard/components/pipeline/vulnerability_finding_modal_spec.js @@ -35,7 +35,8 @@ import dismissFindingMutation from 'ee/security_dashboard/graphql/mutations/dism import createMergeRequestMutation from 'ee/security_dashboard/graphql/mutations/finding_create_merge_request.mutation.graphql'; import securityFindingRevertToDetected from 'ee/security_dashboard/graphql/mutations/revert_finding_to_detected.mutation.graphql'; import createIssueMutation from 'ee/security_dashboard/graphql/mutations/finding_create_issue.mutation.graphql'; -import createExternalIssueMutation from 'ee/security_dashboard/graphql/mutations/vulnerability_create_jira_issue.mutation.graphql'; +import vulnerabilityCreateExternalIssueMutation from 'ee/security_dashboard/graphql/mutations/vulnerability_create_jira_issue.mutation.graphql'; +import findingCreateExternalIssueMutation from 'ee/security_dashboard/graphql/mutations/finding_create_jira_issue.mutation.graphql'; import { VULNERABILITY_TAB_NAMES } from 'ee/vulnerabilities/constants'; import VulnerabilityCodeFlow from 'ee/vue_shared/components/code_flow/vulnerability_code_flow.vue'; import { @@ -46,6 +47,7 @@ import { securityFindingRevertToDetectedMutationResponse, securityFindingCreateMergeRequestMutationResponse, securityFindingCreateIssueMutationResponse, + vulnerabilityCreateExternalIssueMutationResponse, securityFindingCreateExternalIssueMutationResponse, } from './mock_data'; @@ -114,7 +116,12 @@ describe('ee/security_dashboard/components/pipeline/vulnerability_finding_modal. jest.fn().mockResolvedValue(securityFindingCreateIssueMutationResponse), ], [ - createExternalIssueMutation, + vulnerabilityCreateExternalIssueMutation, + handlers.createExternalIssueMutation || + jest.fn().mockResolvedValue(vulnerabilityCreateExternalIssueMutationResponse), + ], + [ + findingCreateExternalIssueMutation, handlers.createExternalIssueMutation || jest.fn().mockResolvedValue(securityFindingCreateExternalIssueMutationResponse), ], @@ -127,6 +134,7 @@ describe('ee/security_dashboard/components/pipeline/vulnerability_finding_modal. responseHandlers, propsData, glAbilities: { resolveVulnerabilityWithAi = true } = {}, + glFeatures: { findingCreateJiraIssueMutation = true } = {}, ...options } = {}) => { modalMockMethods = { @@ -157,6 +165,7 @@ describe('ee/security_dashboard/components/pipeline/vulnerability_finding_modal. }), provide: { glAbilities: { resolveVulnerabilityWithAi }, + glFeatures: { findingCreateJiraIssueMutation }, vulnerabilitiesQuery: vulnerabilitiesQueryHandler, }, ...options, @@ -1450,7 +1459,8 @@ describe('ee/security_dashboard/components/pipeline/vulnerability_finding_modal. await createIssueEvent(); expect(createExternalIssueMutationHandler).toHaveBeenCalledWith({ - vulnerabilityId: pipelineSecurityReportFinding.vulnerability.id, + findingUuid: pipelineSecurityReportFinding.uuid, + projectId: pipelineSecurityReportFinding.project.id, }); }); @@ -1464,6 +1474,43 @@ describe('ee/security_dashboard/components/pipeline/vulnerability_finding_modal. expect(visitUrl).toHaveBeenCalledWith('https://jira.com/1'); }); }); + + describe('with feature flag `findingCreateJiraIssueMutation` disabled', () => { + beforeEach(async () => { + createExternalIssueMutationHandler = jest + .fn() + .mockResolvedValue(vulnerabilityCreateExternalIssueMutationResponse); + + createWrapper({ + glFeatures: { + findingCreateJiraIssueMutation: false, + }, + responseHandlers: { + createExternalIssueMutation: createExternalIssueMutationHandler, + securityReportFindingQuery: jest.fn().mockResolvedValue( + getPipelineSecurityReportFindingResponse({ + overrides: { + project: { + hasJiraVulnerabilityIssueCreationEnabled: true, + }, + }, + }), + ), + }, + }); + await waitForPromises(); + }); + + it('should call the vulnerability mutation when the button is clicked with the correct paras', async () => { + expect(createExternalIssueMutationHandler).not.toHaveBeenCalled(); + + await clickCreateJiraIssueButton(); + + expect(createExternalIssueMutationHandler).toHaveBeenCalledWith({ + vulnerabilityId: pipelineSecurityReportFinding.vulnerability.id, + }); + }); + }); }); describe('error', () => { diff --git a/ee/spec/graphql/mutations/security/finding/create_external_issue_link_spec.rb b/ee/spec/graphql/mutations/security/finding/create_external_issue_link_spec.rb new file mode 100644 index 00000000000000..a9d2c48f8b2ee4 --- /dev/null +++ b/ee/spec/graphql/mutations/security/finding/create_external_issue_link_spec.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Mutations::Security::Finding::CreateExternalIssueLink, feature_category: :vulnerability_management do + include GraphqlHelpers + let(:mutation) { described_class.new(object: nil, context: query_context, field: nil) } + + describe '#resolve' do + let_it_be(:project) { create(:project) } + let_it_be(:current_user) { create(:user) } + let_it_be(:security_finding) { create(:security_finding, :with_finding_data) } + + context 'for JIRA external tracker and CREATED issue link' do + subject(:resolve_mutation) do + mutation.resolve( + uuid: security_finding.uuid, + project: GitlabSchema.id_from_object(project), + link_type: 'created', + external_tracker: 'jira' + ) + end + + context 'when the project can have external issue links' do + before do + stub_licensed_features(security_dashboard: true) + end + + context 'when user does not have access to the project' do + it 'raises an error' do + expect { resolve_mutation }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end + + context 'when user has access to the project' do + before_all do + project.add_developer(current_user) + end + + before do + allow_next_instance_of(::Security::Findings::CreateExternalIssueLinkService) do |create_service| + allow(create_service).to receive(:execute).and_return(result) + end + end + + context 'when issue creation fails' do + let(:result) do + instance_double(ServiceResponse, success?: false, payload: {}, + errors: ['Error when creating issue in Jira']) + end + + it 'returns empty external issue link' do + expect(resolve_mutation[:external_issue_link]).to be_nil + end + + it 'returns error collection' do + expect(resolve_mutation[:errors]).to eq(['Error when creating issue in Jira']) + end + end + + context 'when issue creation succeeds' do + let_it_be(:external_issue_link) { build(:vulnerabilities_external_issue_link) } + + let(:result) do + instance_double(ServiceResponse, success?: true, payload: { record: external_issue_link }, errors: []) + end + + it 'returns the external issue link' do + expect(resolve_mutation[:external_issue_link]).to eq(external_issue_link) + end + + it 'returns empty error collection' do + expect(resolve_mutation[:errors]).to be_empty + end + end + end + end + end + end +end diff --git a/ee/spec/services/security/findings/create_external_issue_link_service_spec.rb b/ee/spec/services/security/findings/create_external_issue_link_service_spec.rb new file mode 100644 index 00000000000000..7e16726b9f267c --- /dev/null +++ b/ee/spec/services/security/findings/create_external_issue_link_service_spec.rb @@ -0,0 +1,199 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Security::Findings::CreateExternalIssueLinkService, feature_category: :vulnerability_management do + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project) } + let_it_be(:pipeline) { create(:ee_ci_pipeline, project: project) } + let(:scan) { create(:security_scan, pipeline: pipeline, project: project) } + let(:security_finding) { create(:security_finding, scan: scan) } + let(:vulnerability) { create(:vulnerability, project: project) } + let(:external_tracker) { 'jira' } + let(:link_type) { 'created' } + + let(:params) do + { + security_finding_uuid: security_finding.uuid, + external_tracker: external_tracker, + link_type: link_type + } + end + + let(:service) { described_class.new(project: project, current_user: user, params: params) } + + subject(:execute_service) { service.execute } + + before_all do + project.add_developer(user) + end + + describe '#execute' do + let(:vulnerability_service_response) { ServiceResponse.success(payload: { vulnerability: vulnerability }) } + let(:external_link_service_response) { ServiceResponse.success(payload: { record: external_issue_link }) } + let(:external_issue_link) do + create(:vulnerabilities_external_issue_link, + vulnerability: vulnerability, + external_type: external_tracker, + link_type: link_type) + end + + before do + allow_next_instance_of(Vulnerabilities::FindOrCreateFromSecurityFindingService) do |service| + allow(service).to receive(:execute).and_return(vulnerability_service_response) + end + + allow_next_instance_of(VulnerabilityExternalIssueLinks::CreateService) do |service| + allow(service).to receive(:execute).and_return(external_link_service_response) + end + end + + context 'when both services succeed' do + it 'returns a success response' do + expect(execute_service).to be_success + end + + it 'returns the external issue link in the payload' do + result = execute_service + expect(result.payload[:record]).to eq(external_issue_link) + end + + it 'calls FindOrCreateFromSecurityFindingService with correct params' do + expect(Vulnerabilities::FindOrCreateFromSecurityFindingService).to receive(:new).with( + project: project, + current_user: user, + params: params, + present_on_default_branch: false, + state: 'detected' + ) + + execute_service + end + + it 'calls VulnerabilityExternalIssueLinks::CreateService with correct params' do + expect(VulnerabilityExternalIssueLinks::CreateService).to receive(:new).with( + user, + vulnerability, + external_tracker, + link_type: link_type + ) + + execute_service + end + end + + context 'when vulnerability creation fails' do + let(:error_message) { 'Unable to create vulnerability' } + let(:vulnerability_service_response) { ServiceResponse.error(message: error_message) } + + it 'returns an error response' do + expect(execute_service).not_to be_success + end + + it 'returns the error message' do + result = execute_service + expect(result.message).to eq(error_message) + end + + it 'does not call VulnerabilityExternalIssueLinks::CreateService' do + expect(VulnerabilityExternalIssueLinks::CreateService).not_to receive(:new) + + execute_service + end + end + + context 'when external issue link creation fails' do + let(:error_message) { ['External provider service is not configured to create issues.'] } + let(:external_link_service_response) { ServiceResponse.error(message: error_message) } + + it 'returns an error response' do + expect(execute_service).not_to be_success + end + + it 'returns the error message' do + result = execute_service + expect(result.message).to eq(error_message) + end + end + + context 'when user does not have permission' do + before_all do + project.add_guest(user) + end + + context 'when vulnerability service checks permissions' do + let(:vulnerability_service_response) do + ServiceResponse.error(message: 'Insufficient permissions') + end + + it 'returns an error' do + expect(execute_service).not_to be_success + expect(execute_service.message).to eq('Insufficient permissions') + end + end + end + + context 'with missing params' do + context 'when security_finding_uuid is missing' do + let(:params) do + { + external_tracker: external_tracker, + link_type: link_type + } + end + + it 'passes nil to vulnerability service' do + expect(Vulnerabilities::FindOrCreateFromSecurityFindingService).to receive(:new).with( + project: project, + current_user: user, + params: params, + present_on_default_branch: false, + state: 'detected' + ) + + execute_service + end + end + + context 'when external_tracker is missing' do + let(:params) do + { + security_finding_uuid: security_finding.uuid, + link_type: link_type + } + end + + it 'passes nil to external issue link service' do + expect(VulnerabilityExternalIssueLinks::CreateService).to receive(:new).with( + user, + vulnerability, + nil, + link_type: link_type + ) + + execute_service + end + end + + context 'when link_type is missing' do + let(:params) do + { + security_finding_uuid: security_finding.uuid, + external_tracker: external_tracker + } + end + + it 'passes nil as link_type to external issue link service' do + expect(VulnerabilityExternalIssueLinks::CreateService).to receive(:new).with( + user, + vulnerability, + external_tracker, + link_type: nil + ) + + execute_service + end + end + end + end +end -- GitLab From 2f393117500e1b1c345438ecb36279ebc29950e8 Mon Sep 17 00:00:00 2001 From: Lorenz van Herwaarden Date: Mon, 15 Sep 2025 07:55:30 +0200 Subject: [PATCH 02/10] Fix wrong content of vulnerability create jira issue mutation --- .../vulnerability_create_jira_issue.mutation.graphql | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ee/app/assets/javascripts/security_dashboard/graphql/mutations/vulnerability_create_jira_issue.mutation.graphql b/ee/app/assets/javascripts/security_dashboard/graphql/mutations/vulnerability_create_jira_issue.mutation.graphql index 4887ba2b2091f4..44641bc07a1b2a 100644 --- a/ee/app/assets/javascripts/security_dashboard/graphql/mutations/vulnerability_create_jira_issue.mutation.graphql +++ b/ee/app/assets/javascripts/security_dashboard/graphql/mutations/vulnerability_create_jira_issue.mutation.graphql @@ -1,6 +1,6 @@ -mutation securityFindingJiraIssueCreate($findingUuid: String!, $projectId: ProjectID!) { - securityFindingExternalIssueLinkCreate( - input: { uuid: $findingUuid, project: $projectId, linkType: CREATED, externalTracker: JIRA } +mutation vulnerabilityJiraIssueCreate($vulnerabilityId: VulnerabilityID!) { + vulnerabilityExternalIssueLinkCreate( + input: { id: $vulnerabilityId, linkType: CREATED, externalTracker: JIRA } ) { errors externalIssueLink { -- GitLab From 2149b5de51dea71ee71adc147454f81f33bdb803 Mon Sep 17 00:00:00 2001 From: Lorenz van Herwaarden Date: Mon, 15 Sep 2025 10:46:27 +0200 Subject: [PATCH 03/10] Apply 1 suggestion(s) to 1 file(s) Co-authored-by: David Pisek --- .../components/pipeline/vulnerability_finding_modal_spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ee/spec/frontend/security_dashboard/components/pipeline/vulnerability_finding_modal_spec.js b/ee/spec/frontend/security_dashboard/components/pipeline/vulnerability_finding_modal_spec.js index 76bec17032c1be..d437fbf030cabb 100644 --- a/ee/spec/frontend/security_dashboard/components/pipeline/vulnerability_finding_modal_spec.js +++ b/ee/spec/frontend/security_dashboard/components/pipeline/vulnerability_finding_modal_spec.js @@ -1501,7 +1501,7 @@ describe('ee/security_dashboard/components/pipeline/vulnerability_finding_modal. await waitForPromises(); }); - it('should call the vulnerability mutation when the button is clicked with the correct paras', async () => { + it('should call the vulnerability mutation when the button is clicked with the correct params', async () => { expect(createExternalIssueMutationHandler).not.toHaveBeenCalled(); await clickCreateJiraIssueButton(); -- GitLab From bb04ae4636935d4882f923884c94ce5def1311ad Mon Sep 17 00:00:00 2001 From: Lorenz van Herwaarden Date: Mon, 15 Sep 2025 10:46:01 +0200 Subject: [PATCH 04/10] Improve nested if --- .../components/pipeline/vulnerability_finding_modal.vue | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/ee/app/assets/javascripts/security_dashboard/components/pipeline/vulnerability_finding_modal.vue b/ee/app/assets/javascripts/security_dashboard/components/pipeline/vulnerability_finding_modal.vue index 5a239580a47837..f87652dd0aa627 100644 --- a/ee/app/assets/javascripts/security_dashboard/components/pipeline/vulnerability_finding_modal.vue +++ b/ee/app/assets/javascripts/security_dashboard/components/pipeline/vulnerability_finding_modal.vue @@ -487,10 +487,8 @@ export default { const useFindingMutation = this.glFeatures.findingCreateJiraIssueMutation; - if (!useFindingMutation) { - if (!vulnerabilityId) { - return; - } + if (!useFindingMutation && !vulnerabilityId) { + return; } const { createJiraIssueErrorMessage } = this.$options.i18n; -- GitLab From d501344678f564aeddd3a44c53d3a574b6d91491 Mon Sep 17 00:00:00 2001 From: Lorenz van Herwaarden Date: Mon, 15 Sep 2025 17:59:45 +0200 Subject: [PATCH 05/10] Add actual integration spec for new mutation and add vulnerability destroy --- .../finding/create_external_issue_link.rb | 2 +- .../create_external_issue_link_service.rb | 31 ++- .../finding/create_external_issue_spec.rb | 210 ++++++++++++++++++ 3 files changed, 230 insertions(+), 13 deletions(-) create mode 100644 ee/spec/requests/api/graphql/mutations/security/finding/create_external_issue_spec.rb diff --git a/ee/app/graphql/mutations/security/finding/create_external_issue_link.rb b/ee/app/graphql/mutations/security/finding/create_external_issue_link.rb index 3c8d1d9a2f42b2..f185af3ebf60f9 100644 --- a/ee/app/graphql/mutations/security/finding/create_external_issue_link.rb +++ b/ee/app/graphql/mutations/security/finding/create_external_issue_link.rb @@ -6,7 +6,7 @@ module Finding class CreateExternalIssueLink < BaseMutation graphql_name 'SecurityFindingExternalIssueLinkCreate' - authorize :read_security_resource + authorize :admin_vulnerability_external_issue_link field :external_issue_link, Types::Vulnerability::ExternalIssueLinkType, null: true, diff --git a/ee/app/services/security/findings/create_external_issue_link_service.rb b/ee/app/services/security/findings/create_external_issue_link_service.rb index d7285264069dfa..e12e1c093a6ad5 100644 --- a/ee/app/services/security/findings/create_external_issue_link_service.rb +++ b/ee/app/services/security/findings/create_external_issue_link_service.rb @@ -3,25 +3,22 @@ module Security module Findings class CreateExternalIssueLinkService < ::BaseProjectService - attr_reader :error_message + include Gitlab::Utils::StrongMemoize def execute - vulnerability_response = create_vulnerability return error(vulnerability_response[:message]) if vulnerability_response.error? - vulnerability = vulnerability_response.payload[:vulnerability] + if external_issue_link_response.error? + vulnerability.destroy if vulnerability.present? + return error(external_issue_link_response[:message]) + end - external_issue_link_response = create_external_issue_link(vulnerability) - return error(external_issue_link_response[:message]) if external_issue_link_response.error? - - external_issue_link = external_issue_link_response.payload[:record] - - success(external_issue_link) + success end private - def create_vulnerability + def vulnerability_response Vulnerabilities::FindOrCreateFromSecurityFindingService.new( project: @project, current_user: @current_user, @@ -29,20 +26,30 @@ def create_vulnerability present_on_default_branch: false, state: 'detected').execute end + strong_memoize_attr :vulnerability_response + + def vulnerability + vulnerability_response.payload[:vulnerability] + end - def create_external_issue_link(vulnerability) + def external_issue_link_response ::VulnerabilityExternalIssueLinks::CreateService.new( @current_user, vulnerability, params[:external_tracker], link_type: params[:link_type]).execute end + strong_memoize_attr :external_issue_link_response + + def external_issue_link + external_issue_link_response.payload[:record] + end def error(message) ServiceResponse.error(message: message) end - def success(external_issue_link) + def success ServiceResponse.success(payload: { record: external_issue_link }) end end diff --git a/ee/spec/requests/api/graphql/mutations/security/finding/create_external_issue_spec.rb b/ee/spec/requests/api/graphql/mutations/security/finding/create_external_issue_spec.rb new file mode 100644 index 00000000000000..514f71a0ecdef9 --- /dev/null +++ b/ee/spec/requests/api/graphql/mutations/security/finding/create_external_issue_spec.rb @@ -0,0 +1,210 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Creating an External Issue Link from a Security::Finding', feature_category: :vulnerability_management do + include GraphqlHelpers + + before do + stub_licensed_features(security_dashboard: true, jira_vulnerabilities_integration: true) + end + + let_it_be(:current_user) { create(:user) } + let_it_be(:project) { create(:project, :repository) } + let_it_be(:pipeline) { create(:ci_pipeline, :success, project: project) } + + let_it_be(:build_sast) { create(:ci_build, :success, name: 'sast', pipeline: pipeline) } + let_it_be(:artifact_sast) do + create(:ee_ci_job_artifact, :sast, job: build_sast) + end + + let_it_be(:report) { create(:ci_reports_security_report, pipeline: pipeline, type: :sast) } + let_it_be(:scan) { create(:security_scan, :latest_successful, scan_type: :sast, build: artifact_sast.job) } + + let_it_be(:security_findings) { [] } + + let(:security_finding) { security_findings.first } + let(:security_finding_uuid) { security_finding.uuid } + let(:project_gid) { GitlabSchema.id_from_object(project) } + + let(:mutation_name) { :security_finding_external_issue_link_create } + let(:mutation) do + graphql_mutation( + mutation_name, + project: project_gid, + uuid: security_finding_uuid, + link_type: 'CREATED', + external_tracker: 'JIRA' + ) + end + + def mutation_response + graphql_mutation_response(mutation_name) + end + + before_all do + sast_content = File.read(artifact_sast.file.path) + Gitlab::Ci::Parsers::Security::Sast.parse!(sast_content, report) + report.merge!(report) + security_findings.push(*insert_security_findings) + end + + context 'when the user does not have permission' do + it_behaves_like 'a mutation that returns a top-level access error' + + it 'does not create a new vulnerability' do + expect { post_graphql_mutation(mutation, current_user: current_user) }.not_to change { Vulnerability.count } + end + + it 'does not create a new external issue link' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + end.not_to change { Vulnerabilities::ExternalIssueLink.count } + end + end + + context 'when the user has permission' do + before_all do + project.add_maintainer(current_user) + end + + context 'when security_dashboard is disabled' do + before do + stub_licensed_features(security_dashboard: false) + end + + it_behaves_like 'a mutation that returns top-level errors', + errors: ['The resource that you are attempting to access does not ' \ + 'exist or you don\'t have permission to perform this action'] + end + + context 'when security_dashboard is enabled' do + context 'when jira is not configured' do + it 'responds with error' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(mutation_response['errors']) + .to include('External provider service is not configured to create issues.') + end + + it 'does not create a new vulnerability' do + expect { post_graphql_mutation(mutation, current_user: current_user) }.not_to change { Vulnerability.count } + end + + it 'does not create an external issue link' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + end.not_to change { Vulnerabilities::ExternalIssueLink.count } + end + end + + context 'when jira is configured' do + let!(:jira_integration) do + create(:jira_integration, + project: project, + vulnerabilities_enabled: true, + project_key: 'GV', + vulnerabilities_issuetype: '10000') + end + + context 'when issue creation succeeds' do + before do + stub_request(:get, 'https://jira.example.com/rest/api/2/project/GV') + .to_return(status: 200, body: { 'id' => '10000' }.to_json) + stub_request(:post, 'https://jira.example.com/rest/api/2/issue') + .to_return( + status: 200, + body: jira_created_issue.to_json + ) + end + + let(:jira_created_issue) do + { + 'id' => external_issue_id, + 'key' => 'GV-123', + fields: { + project: { id: '10000' }, + issuetype: { id: '10000' }, + summary: "A summary", + description: "Security finding from GitLab", + created: '2025-01-15T10:00:00.000+0000', + updated: '2025-01-15T10:00:00.000+0000', + resolutiondate: '2020-06-27T13:23:51.000+0000', + labels: ['backend'], + status: { name: 'To Do' }, + key: 'GV-6', + assignee: nil, + reporter: { name: 'admin', displayName: 'Admin' } + } + } + end + + context 'and saving external issue link succeeds' do + let(:external_issue_id) { '10000' } + + it 'does create a new vulnerability' do + expect { post_graphql_mutation(mutation, current_user: current_user) } + .to change { Vulnerability.count }.by(1) + end + + it 'creates the external issue link and returns nil for external issue to be fetched using query', + :aggregate_failures do + expect { post_graphql_mutation(mutation, current_user: current_user) } + .to change { Vulnerabilities::ExternalIssueLink.count }.by(1) + expect(mutation_response['errors']).to be_empty + expect(mutation_response.dig('externalIssueLink', 'externalIssue')).to be_nil + end + end + + context 'and saving external issue link fails' do + let(:external_issue_id) { nil } + + it 'does not create a new vulnerability' do + expect { post_graphql_mutation(mutation, current_user: current_user) } + .not_to change { Vulnerability.count } + end + + it 'does not create the external issue link' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + end.not_to change { Vulnerabilities::ExternalIssueLink.count } + end + + it 'returns errors' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(mutation_response['errors']).not_to be_empty + expect(mutation_response['externalIssueLink']).to be_nil + end + end + end + + context 'when issue creation fails' do + before do + stub_request(:get, 'https://jira.example.com/rest/api/2/project/GV') + .to_return(status: 200, body: { 'id' => '10000' }.to_json) + stub_request(:post, 'https://jira.example.com/rest/api/2/issue') + .to_return(status: 400, body: { 'errors' => ['bad request'] }.to_json) + end + + it 'does not create the external issue link' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + end.not_to change { Vulnerabilities::ExternalIssueLink.count } + end + end + end + end + end + + def insert_security_findings + report.findings.map do |finding| + create( + :security_finding, + severity: finding.severity, + uuid: finding.uuid, + scan: scan + ) + end + end +end -- GitLab From e723a9bc60d9480e8ff6cd788e1246663feb95b2 Mon Sep 17 00:00:00 2001 From: Lorenz van Herwaarden Date: Mon, 15 Sep 2025 18:03:30 +0200 Subject: [PATCH 06/10] Fix params following suggestion --- .../security/finding/create_external_issue_link.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ee/app/graphql/mutations/security/finding/create_external_issue_link.rb b/ee/app/graphql/mutations/security/finding/create_external_issue_link.rb index f185af3ebf60f9..c97234fc44c54f 100644 --- a/ee/app/graphql/mutations/security/finding/create_external_issue_link.rb +++ b/ee/app/graphql/mutations/security/finding/create_external_issue_link.rb @@ -31,9 +31,9 @@ class CreateExternalIssueLink < BaseMutation required: true, description: 'External tracker type of the external issue link.' - def resolve(uuid:, project:, link_type:, external_tracker:) - project = authorized_find!(id: project) - params = { security_finding_uuid: uuid, link_type: link_type, external_tracker: external_tracker } + def resolve(**params) + project = authorized_find!(id: params[:project]) + params[:security_finding_uuid] = params.delete(:uuid) result = ::Security::Findings::CreateExternalIssueLinkService.new(project: project, current_user: current_user, -- GitLab From 269d2d6ecdd33b0204baaec6f2fb9a6b66091b41 Mon Sep 17 00:00:00 2001 From: Lorenz van Herwaarden Date: Mon, 15 Sep 2025 18:06:37 +0200 Subject: [PATCH 07/10] Remove unnecessary spec --- .../create_external_issue_link_spec.rb | 80 ------------------- 1 file changed, 80 deletions(-) delete mode 100644 ee/spec/graphql/mutations/security/finding/create_external_issue_link_spec.rb diff --git a/ee/spec/graphql/mutations/security/finding/create_external_issue_link_spec.rb b/ee/spec/graphql/mutations/security/finding/create_external_issue_link_spec.rb deleted file mode 100644 index a9d2c48f8b2ee4..00000000000000 --- a/ee/spec/graphql/mutations/security/finding/create_external_issue_link_spec.rb +++ /dev/null @@ -1,80 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Mutations::Security::Finding::CreateExternalIssueLink, feature_category: :vulnerability_management do - include GraphqlHelpers - let(:mutation) { described_class.new(object: nil, context: query_context, field: nil) } - - describe '#resolve' do - let_it_be(:project) { create(:project) } - let_it_be(:current_user) { create(:user) } - let_it_be(:security_finding) { create(:security_finding, :with_finding_data) } - - context 'for JIRA external tracker and CREATED issue link' do - subject(:resolve_mutation) do - mutation.resolve( - uuid: security_finding.uuid, - project: GitlabSchema.id_from_object(project), - link_type: 'created', - external_tracker: 'jira' - ) - end - - context 'when the project can have external issue links' do - before do - stub_licensed_features(security_dashboard: true) - end - - context 'when user does not have access to the project' do - it 'raises an error' do - expect { resolve_mutation }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) - end - end - - context 'when user has access to the project' do - before_all do - project.add_developer(current_user) - end - - before do - allow_next_instance_of(::Security::Findings::CreateExternalIssueLinkService) do |create_service| - allow(create_service).to receive(:execute).and_return(result) - end - end - - context 'when issue creation fails' do - let(:result) do - instance_double(ServiceResponse, success?: false, payload: {}, - errors: ['Error when creating issue in Jira']) - end - - it 'returns empty external issue link' do - expect(resolve_mutation[:external_issue_link]).to be_nil - end - - it 'returns error collection' do - expect(resolve_mutation[:errors]).to eq(['Error when creating issue in Jira']) - end - end - - context 'when issue creation succeeds' do - let_it_be(:external_issue_link) { build(:vulnerabilities_external_issue_link) } - - let(:result) do - instance_double(ServiceResponse, success?: true, payload: { record: external_issue_link }, errors: []) - end - - it 'returns the external issue link' do - expect(resolve_mutation[:external_issue_link]).to eq(external_issue_link) - end - - it 'returns empty error collection' do - expect(resolve_mutation[:errors]).to be_empty - end - end - end - end - end - end -end -- GitLab From a4bb4539f2d460a96cee25fb9ac85d22aff1e35f Mon Sep 17 00:00:00 2001 From: Lorenz van Herwaarden Date: Mon, 15 Sep 2025 18:14:59 +0200 Subject: [PATCH 08/10] Check that no vulnerability is created when external issue link fails --- .../findings/create_external_issue_link_service_spec.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ee/spec/services/security/findings/create_external_issue_link_service_spec.rb b/ee/spec/services/security/findings/create_external_issue_link_service_spec.rb index 7e16726b9f267c..a42c3dd0a7911f 100644 --- a/ee/spec/services/security/findings/create_external_issue_link_service_spec.rb +++ b/ee/spec/services/security/findings/create_external_issue_link_service_spec.rb @@ -114,6 +114,10 @@ result = execute_service expect(result.message).to eq(error_message) end + + it 'does not create a new Vulnerability' do + expect(execute_service).not_to change { project.vulnerabilities.count } + end end context 'when user does not have permission' do -- GitLab From 487a22ac766691f0c71ef9a668810857a0425592 Mon Sep 17 00:00:00 2001 From: Lorenz van Herwaarden Date: Mon, 15 Sep 2025 18:23:46 +0200 Subject: [PATCH 09/10] Remove vulnerability destroy again --- .../findings/create_external_issue_link_service.rb | 5 +---- ..._issue_spec.rb => create_external_issue_link_spec.rb} | 9 --------- .../findings/create_external_issue_link_service_spec.rb | 4 ---- 3 files changed, 1 insertion(+), 17 deletions(-) rename ee/spec/requests/api/graphql/mutations/security/finding/{create_external_issue_spec.rb => create_external_issue_link_spec.rb} (94%) diff --git a/ee/app/services/security/findings/create_external_issue_link_service.rb b/ee/app/services/security/findings/create_external_issue_link_service.rb index e12e1c093a6ad5..51b12a015f64bd 100644 --- a/ee/app/services/security/findings/create_external_issue_link_service.rb +++ b/ee/app/services/security/findings/create_external_issue_link_service.rb @@ -8,10 +8,7 @@ class CreateExternalIssueLinkService < ::BaseProjectService def execute return error(vulnerability_response[:message]) if vulnerability_response.error? - if external_issue_link_response.error? - vulnerability.destroy if vulnerability.present? - return error(external_issue_link_response[:message]) - end + return error(external_issue_link_response[:message]) if external_issue_link_response.error? success end diff --git a/ee/spec/requests/api/graphql/mutations/security/finding/create_external_issue_spec.rb b/ee/spec/requests/api/graphql/mutations/security/finding/create_external_issue_link_spec.rb similarity index 94% rename from ee/spec/requests/api/graphql/mutations/security/finding/create_external_issue_spec.rb rename to ee/spec/requests/api/graphql/mutations/security/finding/create_external_issue_link_spec.rb index 514f71a0ecdef9..914cbd66642916 100644 --- a/ee/spec/requests/api/graphql/mutations/security/finding/create_external_issue_spec.rb +++ b/ee/spec/requests/api/graphql/mutations/security/finding/create_external_issue_link_spec.rb @@ -87,10 +87,6 @@ def mutation_response .to include('External provider service is not configured to create issues.') end - it 'does not create a new vulnerability' do - expect { post_graphql_mutation(mutation, current_user: current_user) }.not_to change { Vulnerability.count } - end - it 'does not create an external issue link' do expect do post_graphql_mutation(mutation, current_user: current_user) @@ -159,11 +155,6 @@ def mutation_response context 'and saving external issue link fails' do let(:external_issue_id) { nil } - it 'does not create a new vulnerability' do - expect { post_graphql_mutation(mutation, current_user: current_user) } - .not_to change { Vulnerability.count } - end - it 'does not create the external issue link' do expect do post_graphql_mutation(mutation, current_user: current_user) diff --git a/ee/spec/services/security/findings/create_external_issue_link_service_spec.rb b/ee/spec/services/security/findings/create_external_issue_link_service_spec.rb index a42c3dd0a7911f..7e16726b9f267c 100644 --- a/ee/spec/services/security/findings/create_external_issue_link_service_spec.rb +++ b/ee/spec/services/security/findings/create_external_issue_link_service_spec.rb @@ -114,10 +114,6 @@ result = execute_service expect(result.message).to eq(error_message) end - - it 'does not create a new Vulnerability' do - expect(execute_service).not_to change { project.vulnerabilities.count } - end end context 'when user does not have permission' do -- GitLab From 78f2f13c808362a013e2d84a1a2d611b9496b8ce Mon Sep 17 00:00:00 2001 From: Lorenz van Herwaarden Date: Wed, 17 Sep 2025 09:17:07 +0200 Subject: [PATCH 10/10] Improve create security findings in spec --- .../finding/create_external_issue_link_spec.rb | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/ee/spec/requests/api/graphql/mutations/security/finding/create_external_issue_link_spec.rb b/ee/spec/requests/api/graphql/mutations/security/finding/create_external_issue_link_spec.rb index 914cbd66642916..b0f18010206f25 100644 --- a/ee/spec/requests/api/graphql/mutations/security/finding/create_external_issue_link_spec.rb +++ b/ee/spec/requests/api/graphql/mutations/security/finding/create_external_issue_link_spec.rb @@ -18,10 +18,9 @@ create(:ee_ci_job_artifact, :sast, job: build_sast) end - let_it_be(:report) { create(:ci_reports_security_report, pipeline: pipeline, type: :sast) } let_it_be(:scan) { create(:security_scan, :latest_successful, scan_type: :sast, build: artifact_sast.job) } - let_it_be(:security_findings) { [] } + let_it_be(:security_findings) { create_security_findings } let(:security_finding) { security_findings.first } let(:security_finding_uuid) { security_finding.uuid } @@ -42,13 +41,6 @@ def mutation_response graphql_mutation_response(mutation_name) end - before_all do - sast_content = File.read(artifact_sast.file.path) - Gitlab::Ci::Parsers::Security::Sast.parse!(sast_content, report) - report.merge!(report) - security_findings.push(*insert_security_findings) - end - context 'when the user does not have permission' do it_behaves_like 'a mutation that returns a top-level access error' @@ -188,7 +180,11 @@ def mutation_response end end - def insert_security_findings + def create_security_findings + report = create(:ci_reports_security_report, pipeline: pipeline, type: :sast) + sast_content = File.read(artifact_sast.file.path) + Gitlab::Ci::Parsers::Security::Sast.parse!(sast_content, report) + report.merge!(report) report.findings.map do |finding| create( :security_finding, -- GitLab