Commit 447536a2 authored by Jonathan Schafer's avatar Jonathan Schafer

Add migration for dimissal data

This change will migrate the dismissal_feedback
`author.id` and `created_at` fields to
vulnerabilities `dissmissed_by_id` and
`dismissed_at` fields, respectively.
parent 5f304f1f
# frozen_string_literal: true
# See https://docs.gitlab.com/ee/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class MigrateVulnerabilityDismissalFeedback < ActiveRecord::Migration[6.0]
# Uncomment the following include if you require helper functions:
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
# When a migration requires downtime you **must** uncomment the following
# constant and define a short and easy to understand explanation as to why the
# migration requires downtime.
# DOWNTIME_REASON = ''
# When using the methods "add_concurrent_index", "remove_concurrent_index" or
# "add_column_with_default" you must disable the use of transactions
# as these methods can not run in an existing transaction.
# When using "add_concurrent_index" or "remove_concurrent_index" methods make sure
# that either of them is the _only_ method called in the migration,
# any other changes should go in a separate migration.
# This ensures that upon failure _only_ the index creation or removing fails
# and can be retried or reverted easily.
#
# To disable transactions uncomment the following line and remove these
# comments:
disable_ddl_transaction!
MIGRATION = 'UpdateVulnerabilitiesFromDismissalFeedback'.freeze
BATCH_SIZE = 500
DELAY_INTERVAL = 2.minutes.to_i
class Vulnerability < ActiveRecord::Base
self.table_name = 'vulnerabilities'
self.inheritance_column = :_type_disabled
include ::EachBatch
end
def up
return unless Gitlab.ee?
Vulnerability.select('project_id').group(:project_id).each_batch(of: BATCH_SIZE, column: "project_id") do |project_batch, index|
batch_delay = (index - 1) * BATCH_SIZE * DELAY_INTERVAL
project_batch.each_with_index do |project, project_batch_index|
project_delay = project_batch_index * DELAY_INTERVAL
migrate_in(batch_delay + project_delay, MIGRATION, project[:project_id])
end
end
end
def down
# nothing to do
end
end
---
title: Migrate dismissal data to Vulnersabilities from Vulnerability Feedback
merge_request: 32444
author:
type: fixed
# frozen_string_literal: true
module EE
module Gitlab
module BackgroundMigration
# This migration updates the dismissed_by_id and dismissed_at properties
# of dimissed vulnerabilities records
module UpdateVulnerabilitiesFromDismissalFeedback
extend ::Gitlab::Utils::Override
VULNERABILITY_DISMISSED = 2
VULNERABILITY_FEEDBACK_DISMISSAL = 0
class Project < ActiveRecord::Base
self.table_name = 'projects'
self.inheritance_column = :_type_disabled
end
override :perform
def perform(project_id)
project = Project.find_by(id: project_id)
return unless project
return if project.archived? || project.pending_delete?
#binding.pry
update_vulnerability_from_dismissal_feedback(project.id)
end
private
def update_vulnerability_from_dismissal_feedback(project_id)
update_vulnerability_from_dismissal_feedback_sql = <<-SQL
UPDATE vulnerabilities AS v
SET dismissed_by_id = vf.author_id, dismissed_at = vf.created_at
FROM vulnerability_occurrences AS vo, vulnerability_feedback AS vf
WHERE vo.vulnerability_id = v.id
AND v.state = #{VULNERABILITY_DISMISSED}
AND vo.project_id = vf.project_id
AND ENCODE(vo.project_fingerprint, 'HEX') = vf.project_fingerprint
AND vo.project_id = #{project_id}
AND vo.report_type = vf.category
AND vf.feedback_type = #{VULNERABILITY_FEEDBACK_DISMISSAL};
SQL
connection.execute(update_vulnerability_from_dismissal_feedback_sql)
rescue => e
logger.warn(
message: 'update_vulnerability_from_dismissal_feedback errored out',
project_id: project_id,
error: e.message
)
end
def connection
@connection ||= ActiveRecord::Base.connection
end
def logger
@logger ||= ::Gitlab::BackgroundMigration::Logger.build
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::BackgroundMigration::UpdateVulnerabilitiesFromDismissalFeedback, :migration, schema: 20200519201128 do
let(:users) { table(:users) }
let(:projects) { table(:projects) }
let(:vulnerabilities) { table(:vulnerabilities) }
let(:pipelines) { table(:ci_pipelines) }
let(:vulnerability_occurrences) { table(:vulnerability_occurrences) }
let(:scanners) { table(:vulnerability_scanners) }
let(:identifiers) { table(:vulnerability_identifiers) }
let(:feedback) { table(:vulnerability_feedback) }
let(:namespaces) { table(:namespaces)}
let(:severity) { Vulnerabilities::Occurrence::SEVERITY_LEVELS[:unknown] }
let(:confidence) { Vulnerabilities::Occurrence::CONFIDENCE_LEVELS[:medium] }
let(:report_type) { Vulnerabilities::Occurrence::REPORT_TYPES[:sast] }
let!(:user) { users.create!(id: 13, email: 'author@example.com', username: 'author', projects_limit: 10) }
let!(:project) { projects.create!(id: 123, namespace_id: namespace.id, name: 'gitlab', path: 'gitlab') }
let(:namespace) do
namespaces.create!(id: 12, name: 'namespace', path: '/path', description: 'description')
end
let(:scanner) do
scanners.create!(id: 6, project_id: project.id, external_id: 'clair', name: 'Security Scanner')
end
let(:identifier) do
identifiers.create!(id: 7,
project_id: 123,
fingerprint: 'd432c2ad2953e8bd587a3a43b3ce309b5b0154c7',
external_type: 'SECURITY_ID',
external_id: 'SECURITY_0',
name: 'SECURITY_IDENTIFIER 0')
end
context 'vulnerability has been dismissed' do
let!(:vulnerability) { vulnerabilities.create!(vuln_params) }
let!(:pipeline) { pipelines.create!(id: 234, project_id: project.id, ref: 'master', sha: 'adf43c3a', status: :success, user_id: user.id) }
let!(:vulnerability_occurrence) do
vulnerability_occurrences.create!(
id: 1, report_type: vulnerability.report_type, name: 'finding_1',
primary_identifier_id: identifier.id, uuid: 'abc', project_fingerprint: 'abc123',
location_fingerprint: 'abc456', project_id: project.id, scanner_id: scanner.id, severity: severity,
confidence: confidence, metadata_version: 'sast:1.0', raw_metadata: '{}', vulnerability_id: vulnerability.id)
end
let!(:dismiss_feedback) do
feedback.create!(category: vulnerability_occurrence.report_type, feedback_type: 0,
project_id: project.id, project_fingerprint: vulnerability_occurrence.project_fingerprint.unpack1('H*'),
author_id: user.id)
end
it 'vulnerability should now have a dismissed_by_id' do
expect(vulnerability.dismissed_by_id).to eq(nil)
expect { described_class.new.perform(project.id) }
.to change { vulnerability.reload.dismissed_by_id }
.from(nil)
.to(dismiss_feedback.author_id)
end
it 'vulnerability should now have a dismissed_at' do
expect(vulnerability.dismissed_at).to eq(nil)
expect { described_class.new.perform(project.id) }
.to change { vulnerability.reload.dismissed_at }
.from(nil)
.to(dismiss_feedback.created_at)
end
context 'project is archived' do
let!(:project) { projects.create!(id: 123, namespace_id: namespace.id, name: 'gitlab', path: 'gitlab', archived: true) }
it 'vulnerability dismissed_by_id should remain nil' do
expect(vulnerability.dismissed_by_id).to eq(nil)
expect { described_class.new.perform(project.id) }.not_to change { vulnerability.reload.dismissed_by_id }.from(nil)
end
it 'vulnerability dismissed_at should remain nil' do
expect(vulnerability.dismissed_at).to eq(nil)
expect { described_class.new.perform(project.id) }.not_to change { vulnerability.reload.dismissed_at }.from(nil)
end
end
context 'project is set to be deleted' do
let!(:project) { projects.create!(id: 123, namespace_id: namespace.id, name: 'gitlab', path: 'gitlab', pending_delete: true) }
it 'vulnerability dismissed_by_id should remain nil' do
expect(vulnerability.dismissed_by_id).to eq(nil)
expect { described_class.new.perform(project.id) }.not_to change { vulnerability.reload.dismissed_by_id }.from(nil)
end
it 'vulnerability dismissed_at should remain nil' do
expect(vulnerability.dismissed_at).to eq(nil)
expect { described_class.new.perform(project.id) }.not_to change { vulnerability.reload.dismissed_at }.from(nil)
end
end
end
context 'has not been dismissed' do
let!(:vulnerability) { vulnerabilities.create!(vuln_params.merge({state: 1})) }
it 'vulnerability should not have a dismissed_by_id' do
expect(vulnerability.dismissed_by_id).to be_nil
expect { described_class.new.perform(project.id) }.not_to change { vulnerability.reload.dismissed_by_id }.from(nil)
end
it 'vulnerability should not have a dismissed_at' do
expect(vulnerability.dismissed_at).to be_nil
expect { described_class.new.perform(project.id) }.not_to change { vulnerability.reload.dismissed_at }.from(nil)
end
end
def vuln_params
{
title: 'title',
state: described_class::VULNERABILITY_DISMISSED,
severity: severity,
confidence: confidence,
report_type: report_type,
project_id: project.id,
author_id: user.id
}
end
end
# frozen_string_literal: true
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20200519201128_migrate_vulnerability_dismissal_feedback.rb')
describe MigrateVulnerabilityDismissalFeedback, :migration, :sidekiq do
let(:users) { table(:users) }
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let!(:user) { users.create!(id: 13, email: 'author@example.com', username: 'author', projects_limit: 10) }
let(:namespace) { namespaces.create!(name: 'gitlab', path: 'gitlab-org') }
let(:project_1) { projects.create!(name: 'gitlab', path: 'gitlab-ce', namespace_id: namespace.id) }
let(:project_2) { projects.create!(name: 'gitlab2', path: 'gitlab-ce', namespace_id: namespace.id) }
let(:vulnerabilities) { table(:vulnerabilities) }
let(:dismissed_state) { Gitlab::BackgroundMigration::UpdateVulnerabilitiesFromDismissalFeedback::VULNERABILITY_DISMISSED }
let(:severity) { Vulnerabilities::Occurrence::SEVERITY_LEVELS[:unknown] }
let(:confidence) { Vulnerabilities::Occurrence::CONFIDENCE_LEVELS[:medium] }
let(:report_type) { Vulnerabilities::Occurrence::REPORT_TYPES[:sast] }
before do
stub_const("#{described_class.name}::BATCH_SIZE", 1)
vulnerabilities.create!(vuln_params.merge!({ project_id: project_1.id }) )
vulnerabilities.create!(vuln_params.merge!({ project_id: project_2.id }) )
vulnerabilities.create!(vuln_params.merge!({ project_id: project_2.id }) )
end
context 'EE' do
before do
allow(Gitlab).to receive(:ee?).and_return(true)
end
it 'creates background job for each project' do
migrate!
expect(BackgroundMigrationWorker.jobs.size).to eq 2
end
it 'calls the UpdateVulnerabilitiesFromDismissalFeedback migration' do
expect(BackgroundMigrationWorker).to receive(:perform_in).with(0, 'UpdateVulnerabilitiesFromDismissalFeedback', project_1.id )
expect(BackgroundMigrationWorker).to receive(:perform_in).with(120, 'UpdateVulnerabilitiesFromDismissalFeedback', project_2.id )
migrate!
end
end
context 'FOSS' do
before do
allow(Gitlab).to receive(:ee?).and_return(false)
end
it 'skips migration for FOSS' do
Sidekiq::Testing.fake! do
migrate!
expect(BackgroundMigrationWorker.jobs.size).to eq 0
end
end
end
def vuln_params
{
title: 'title',
state: dismissed_state,
severity: severity,
confidence: confidence,
report_type: report_type,
author_id: user.id
}
end
end
# frozen_string_literal: true
module Gitlab
module BackgroundMigration
# rubocop: disable Style/Documentation
class UpdateVulnerabilitiesFromDismissalFeedback
def perform(project_id)
end
end
end
end
Gitlab::BackgroundMigration::UpdateVulnerabilitiesFromDismissalFeedback.prepend_if_ee('EE::Gitlab::BackgroundMigration::UpdateVulnerabilitiesFromDismissalFeedback')
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