Commit 459c7deb authored by Mehmet Emin INAC's avatar Mehmet Emin INAC

Dismiss vulnerability while dismissing finding

Synchronize the dismissal state between vulnerabilities and findings.
parent edd4c195
......@@ -6,20 +6,23 @@ module Vulnerabilities
class DismissService < BaseService
FindingsDismissResult = Struct.new(:ok?, :finding, :message)
def initialize(current_user, vulnerability, comment = nil)
def initialize(current_user, vulnerability, comment = nil, dismiss_findings: true)
super(current_user, vulnerability)
@comment = comment
@dismiss_findings = dismiss_findings
end
def execute
raise Gitlab::Access::AccessDeniedError unless authorized?
@vulnerability.transaction do
result = dismiss_findings
if dismiss_findings
result = dismiss_vulnerability_findings
unless result.ok?
handle_finding_dismissal_error(result.finding, result.message)
raise ActiveRecord::Rollback
unless result.ok?
handle_finding_dismissal_error(result.finding, result.message)
raise ActiveRecord::Rollback
end
end
update_with_note(@vulnerability, state: Vulnerability.states[:dismissed], dismissed_by: @user, dismissed_at: Time.current)
......@@ -30,6 +33,8 @@ module Vulnerabilities
private
attr_reader :dismiss_findings
def feedback_service_for(finding)
VulnerabilityFeedback::CreateService.new(@project, @user, feedback_params_for(finding))
end
......@@ -40,11 +45,12 @@ module Vulnerabilities
feedback_type: 'dismissal',
project_fingerprint: finding.project_fingerprint,
comment: @comment,
pipeline: @project.latest_pipeline_with_security_reports(only_successful: true)
pipeline: @project.latest_pipeline_with_security_reports(only_successful: true),
dismiss_vulnerability: false
}
end
def dismiss_findings
def dismiss_vulnerability_findings
@vulnerability.findings.each do |finding|
result = feedback_service_for(finding).execute
......
......@@ -2,21 +2,17 @@
module VulnerabilityFeedback
class CreateService < ::BaseService
def execute
vulnerability_feedback = @project.vulnerability_feedback.find_or_init_for(create_params)
include Gitlab::Utils::StrongMemoize
def execute
raise Gitlab::Access::AccessDeniedError unless can?(current_user, :create_vulnerability_feedback, vulnerability_feedback)
if vulnerability_feedback.for_issue? &&
!vulnerability_feedback.vulnerability_data.blank?
create_issue_for(vulnerability_feedback)
elsif vulnerability_feedback.for_merge_request? &&
!vulnerability_feedback.vulnerability_data.blank?
create_merge_request_for(vulnerability_feedback)
if vulnerability_feedback.for_issue? && !vulnerability_feedback.vulnerability_data.blank?
create_issue
elsif vulnerability_feedback.for_merge_request? && !vulnerability_feedback.vulnerability_data.blank?
create_merge_request
else
vulnerability_feedback.save
dismiss_existing_vulnerability
end
if vulnerability_feedback.persisted? && vulnerability_feedback.valid?
......@@ -34,9 +30,15 @@ module VulnerabilityFeedback
private
attr_reader :params
def vulnerability_feedback
@vulnerability_feedback ||= @project.vulnerability_feedback.find_or_init_for(create_params)
end
def create_params
@params[:author] = @current_user
@params.merge(comment_params)
@params.merge(comment_params).except(:dismiss_vulnerability)
end
def comment_params
......@@ -52,7 +54,7 @@ module VulnerabilityFeedback
super().merge(vulnerability_feedback: vulnerability_feedback)
end
def create_issue_for(vulnerability_feedback)
def create_issue
# Wrap Feedback and Issue creation in the same transaction
ActiveRecord::Base.transaction do
result = Issues::CreateFromVulnerabilityDataService
......@@ -80,7 +82,7 @@ module VulnerabilityFeedback
end
end
def create_merge_request_for(vulnerability_feedback)
def create_merge_request
result = MergeRequests::CreateFromVulnerabilityDataService
.new(@project, @current_user, vulnerability_feedback.vulnerability_data)
.execute
......@@ -115,5 +117,26 @@ module VulnerabilityFeedback
.new(current_user, vulnerability, issue, link_type: Vulnerabilities::IssueLink.link_types[:created])
.execute
end
def dismiss_existing_vulnerability
ActiveRecord::Base.transaction do
if dismiss_vulnerability? && existing_vulnerability
Vulnerabilities::DismissService.new(current_user,
existing_vulnerability,
params[:comment],
dismiss_findings: false).execute
end
raise ActiveRecord::Rollback unless vulnerability_feedback.save
end
end
def existing_vulnerability
strong_memoize(:existing_vulnerability) { vulnerability_feedback.finding&.vulnerability }
end
def dismiss_vulnerability?
params[:dismiss_vulnerability] != false && can?(current_user, :admin_vulnerability, project)
end
end
end
......@@ -14,7 +14,8 @@ RSpec.describe Vulnerabilities::DismissService do
let!(:pipeline) { create(:ci_pipeline, :success, project: project) }
let!(:build) { create(:ee_ci_build, :sast, pipeline: pipeline) }
let(:vulnerability) { create(:vulnerability, :with_findings, project: project) }
let(:service) { described_class.new(user, vulnerability) }
let(:dismiss_findings) { true }
let(:service) { described_class.new(user, vulnerability, dismiss_findings: dismiss_findings) }
subject(:dismiss_vulnerability) { service.execute }
......@@ -25,13 +26,29 @@ RSpec.describe Vulnerabilities::DismissService do
it_behaves_like 'calls vulnerability statistics utility services in order'
it 'dismisses a vulnerability and its associated findings' do
Timecop.freeze do
dismiss_vulnerability
context 'when the `dismiss_findings` argument is false' do
let(:dismiss_findings) { false }
expect(vulnerability.reload).to(
have_attributes(state: 'dismissed', dismissed_by: user, dismissed_at: be_like_time(Time.current)))
expect(vulnerability.findings).to all have_vulnerability_dismissal_feedback
it 'dismisses only vulnerability' do
Timecop.freeze do
dismiss_vulnerability
expect(vulnerability.reload).to(
have_attributes(state: 'dismissed', dismissed_by: user, dismissed_at: be_like_time(Time.current)))
expect(vulnerability.findings).not_to include have_vulnerability_dismissal_feedback
end
end
end
context 'when the `dismiss_findings` argument is not false' do
it 'dismisses a vulnerability and its associated findings' do
Timecop.freeze do
dismiss_vulnerability
expect(vulnerability.reload).to(
have_attributes(state: 'dismissed', dismissed_by: user, dismissed_at: be_like_time(Time.current)))
expect(vulnerability.findings).to all have_vulnerability_dismissal_feedback
end
end
end
......@@ -62,7 +79,7 @@ RSpec.describe Vulnerabilities::DismissService do
context 'when there is a finding dismissal error' do
before do
allow(service).to receive(:dismiss_findings).and_return(
allow(service).to receive(:dismiss_vulnerability_findings).and_return(
described_class::FindingsDismissResult.new(false, broken_finding, 'something went wrong'))
end
......
......@@ -7,20 +7,24 @@ RSpec.describe VulnerabilityFeedback::CreateService, '#execute' do
let(:project) { create(:project, :public, :repository, namespace: group) }
let(:user) { create(:user) }
let(:pipeline) { create(:ci_pipeline, project: project) }
let(:dismiss_vulnerability) { true }
before do
group.add_developer(user)
end
context 'when params are valid' do
let(:category) { 'sast' }
let(:project_fingerprint) { '418291a26024a1445b23fe64de9380cdcdfd1fa8' }
let(:feedback_params) do
{
feedback_type: 'dismissal', pipeline_id: pipeline.id, category: 'sast',
project_fingerprint: '418291a26024a1445b23fe64de9380cdcdfd1fa8',
feedback_type: 'dismissal', pipeline_id: pipeline.id, category: category,
project_fingerprint: project_fingerprint,
comment: 'a dismissal comment',
dismiss_vulnerability: dismiss_vulnerability,
vulnerability_data: {
blob_path: '/path/to/blob',
category: 'sast',
category: category,
priority: 'Low', line: '41',
file: 'subdir/src/main/java/com/gitlab/security_products/tests/App.java',
cve: '818bf5dacb291e15d9e6dc3c5ac32178:PREDICTABLE_RANDOM',
......@@ -42,6 +46,13 @@ RSpec.describe VulnerabilityFeedback::CreateService, '#execute' do
context 'when feedback_type is dismissal' do
let(:result) { described_class.new(project, user, feedback_params).execute }
let!(:finding) do
create(:vulnerabilities_finding,
:detected,
project: project,
report_type: category,
project_fingerprint: project_fingerprint)
end
it 'creates the feedback with the given params' do
expect(result[:status]).to eq(:success)
......@@ -51,8 +62,8 @@ RSpec.describe VulnerabilityFeedback::CreateService, '#execute' do
expect(feedback.author).to eq(user)
expect(feedback.feedback_type).to eq('dismissal')
expect(feedback.pipeline_id).to eq(pipeline.id)
expect(feedback.category).to eq('sast')
expect(feedback.project_fingerprint).to eq('418291a26024a1445b23fe64de9380cdcdfd1fa8')
expect(feedback.category).to eq(category)
expect(feedback.project_fingerprint).to eq(project_fingerprint)
expect(feedback.for_dismissal?).to eq(true)
expect(feedback.for_issue?).to eq(false)
expect(feedback.issue).to be_nil
......@@ -87,6 +98,44 @@ RSpec.describe VulnerabilityFeedback::CreateService, '#execute' do
expect(feedback.comment_timestamp).to be_nil
end
end
context 'when the `dismiss_vulnerability` argument is true' do
context 'when the security_dashboard is not enabled' do
it 'does not dismiss the existing vulnerability' do
expect { result }.not_to change { finding.vulnerability.reload.state }.from('detected')
end
end
context 'when the security_dashboard is enabled' do
before do
stub_licensed_features(security_dashboard: true)
end
it 'dismisses the existing vulnerability' do
expect { result }.to change { finding.vulnerability.reload.state }.from('detected').to('dismissed')
end
end
end
context 'when the `dismiss_vulnerability` argument is false' do
let(:dismiss_vulnerability) { false }
context 'when the security_dashboard is not enabled' do
it 'does not dismiss the existing vulnerability' do
expect { result }.not_to change { finding.vulnerability.reload.state }.from('detected')
end
end
context 'when the security_dashboard is enabled' do
before do
stub_licensed_features(security_dashboard: true)
end
it 'dismisses the existing vulnerability' do
expect { result }.not_to change { finding.vulnerability.reload.state }.from('detected')
end
end
end
end
context 'when feedback_type is issue' do
......@@ -145,7 +194,7 @@ RSpec.describe VulnerabilityFeedback::CreateService, '#execute' do
feedback_params.deep_merge(
feedback_type: 'issue',
vulnerability_data: { vulnerability_id: vulnerability_id }
)
).except(:dismiss_vulnerability)
end
subject(:result) { described_class.new(project, user, feedback_params_with_vulnerability_id).execute }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment