Commit d9c4c9e3 authored by Rémy Coutable's avatar Rémy Coutable

Merge branch 'workaround_for_non_unique_project_fingerprints' into 'master'

Deduplicate findings within the artifact itself and use position instead of project_fingerprint

See merge request gitlab-org/gitlab!44815
parents 67c136a6 c287f790
---
title: Add `position` column into security_findings table
merge_request: 44815
author:
type: fixed
# frozen_string_literal: true
class AddPositionIntoSecurityFindings < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
with_lock_retries do
add_column :security_findings, :position, :integer
end
end
def down
with_lock_retries do
remove_column :security_findings, :position
end
end
end
# frozen_string_literal: true
class AddUniqueIndexOnScanIdAndPositionOfSecurityFindings < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
INDEX_NAME = 'index_security_findings_on_scan_id_and_position'
disable_ddl_transaction!
def up
add_concurrent_index :security_findings, [:scan_id, :position], unique: true, name: INDEX_NAME
end
def down
remove_concurrent_index_by_name :security_findings, INDEX_NAME
end
end
d0ca8f0dbe0cf0fbbdd715867f3ae20862683433d919ee5cd942086d21f3b44d
\ No newline at end of file
f19ab0de07415e728849ef4e56804909a3a4a57ad8f55fe71a27bc43c535ac66
\ No newline at end of file
...@@ -15857,6 +15857,7 @@ CREATE TABLE security_findings ( ...@@ -15857,6 +15857,7 @@ CREATE TABLE security_findings (
confidence smallint NOT NULL, confidence smallint NOT NULL,
project_fingerprint text NOT NULL, project_fingerprint text NOT NULL,
deduplicated boolean DEFAULT false NOT NULL, deduplicated boolean DEFAULT false NOT NULL,
"position" integer,
CONSTRAINT check_b9508c6df8 CHECK ((char_length(project_fingerprint) <= 40)) CONSTRAINT check_b9508c6df8 CHECK ((char_length(project_fingerprint) <= 40))
); );
...@@ -21528,6 +21529,8 @@ CREATE INDEX index_security_findings_on_project_fingerprint ON security_findings ...@@ -21528,6 +21529,8 @@ CREATE INDEX index_security_findings_on_project_fingerprint ON security_findings
CREATE INDEX index_security_findings_on_scan_id_and_deduplicated ON security_findings USING btree (scan_id, deduplicated); CREATE INDEX index_security_findings_on_scan_id_and_deduplicated ON security_findings USING btree (scan_id, deduplicated);
CREATE UNIQUE INDEX index_security_findings_on_scan_id_and_position ON security_findings USING btree (scan_id, "position");
CREATE INDEX index_security_findings_on_scanner_id ON security_findings USING btree (scanner_id); CREATE INDEX index_security_findings_on_scanner_id ON security_findings USING btree (scanner_id);
CREATE INDEX index_security_findings_on_severity ON security_findings USING btree (severity); CREATE INDEX index_security_findings_on_severity ON security_findings USING btree (severity);
......
...@@ -118,11 +118,14 @@ module EE ...@@ -118,11 +118,14 @@ module EE
strong_memoize(:security_report) do strong_memoize(:security_report) do
next unless file_type.in?(SECURITY_REPORT_FILE_TYPES) next unless file_type.in?(SECURITY_REPORT_FILE_TYPES)
::Gitlab::Ci::Reports::Security::Report.new(file_type, nil, nil).tap do |report| report = ::Gitlab::Ci::Reports::Security::Report.new(file_type, nil, nil).tap do |report|
each_blob do |blob| each_blob do |blob|
::Gitlab::Ci::Parsers.fabricate!(file_type).parse!(blob, report) ::Gitlab::Ci::Parsers.fabricate!(file_type).parse!(blob, report)
end end
end end
# This will remove the duplicated findings within the artifact itself
::Security::MergeReportsService.new(report).execute
end end
end end
......
...@@ -20,8 +20,9 @@ module Security ...@@ -20,8 +20,9 @@ module Security
enum severity: Vulnerabilities::Finding::SEVERITY_LEVELS, _prefix: :severity enum severity: Vulnerabilities::Finding::SEVERITY_LEVELS, _prefix: :severity
validates :project_fingerprint, presence: true, length: { maximum: 40 } validates :project_fingerprint, presence: true, length: { maximum: 40 }
validates :position, presence: true
scope :by_project_fingerprint, -> (fingerprints) { where(project_fingerprint: fingerprints) } scope :by_position, -> (positions) { where(position: positions) }
scope :by_build_ids, -> (build_ids) { joins(scan: :build).where(ci_builds: { id: build_ids }) } scope :by_build_ids, -> (build_ids) { joins(scan: :build).where(ci_builds: { id: build_ids }) }
end end
end end
...@@ -31,21 +31,22 @@ module Security ...@@ -31,21 +31,22 @@ module Security
end end
def store_findings def store_findings
report_findings.each { |report_finding| store_finding!(report_finding) } report_findings.each_with_index { |report_finding, position| store_finding!(report_finding, position) }
end end
def store_finding!(report_finding) def store_finding!(report_finding, position)
return if report_finding.scanner.blank? return if report_finding.scanner.blank?
security_scan.findings.create!(finding_data(report_finding)) security_scan.findings.create!(finding_data(report_finding, position))
end end
def finding_data(report_finding) def finding_data(report_finding, position)
{ {
severity: report_finding.severity, severity: report_finding.severity,
confidence: report_finding.confidence, confidence: report_finding.confidence,
project_fingerprint: report_finding.project_fingerprint, project_fingerprint: report_finding.project_fingerprint,
scanner: persisted_scanner_for(report_finding.scanner) scanner: persisted_scanner_for(report_finding.scanner),
position: position
} }
end end
......
...@@ -43,21 +43,19 @@ module Security ...@@ -43,21 +43,19 @@ module Security
security_scan.findings.update_all(deduplicated: false) security_scan.findings.update_all(deduplicated: false)
security_scan.findings security_scan.findings
.by_project_fingerprint(deduplicated_project_fingerprints) .by_position(register_finding_keys)
.update_all(deduplicated: true) .update_all(deduplicated: true)
end end
end end
def deduplicated_project_fingerprints # This method registers all finding keys and
register_finding_keys.map(&:project_fingerprint) # returns the positions of unique findings
end
def register_finding_keys def register_finding_keys
@register_finding_keys ||= security_report.findings.select { |finding| register_keys(finding.keys) } @register_finding_keys ||= security_report.findings.map.with_index { |finding, index| register_keys(finding.keys) && index }.compact
end end
def register_keys(keys) def register_keys(keys)
keys.map { |key| known_keys.add?(key) }.all? keys.all? { |key| known_keys.add?(key) }
end end
end end
end end
...@@ -8,5 +8,6 @@ FactoryBot.define do ...@@ -8,5 +8,6 @@ FactoryBot.define do
severity { :critical } severity { :critical }
confidence { :high } confidence { :high }
project_fingerprint { generate(:project_fingerprint) } project_fingerprint { generate(:project_fingerprint) }
sequence :position
end end
end end
...@@ -256,7 +256,9 @@ RSpec.describe Ci::JobArtifact do ...@@ -256,7 +256,9 @@ RSpec.describe Ci::JobArtifact do
clear_security_report clear_security_report
job_artifact.security_report job_artifact.security_report
expect(::Gitlab::Ci::Reports::Security::Report).to have_received(:new).once # This entity class receives the call twice
# because of the way MergeReportsService is implemented.
expect(::Gitlab::Ci::Reports::Security::Report).to have_received(:new).twice
end end
end end
end end
...@@ -10,15 +10,16 @@ RSpec.describe Security::Finding do ...@@ -10,15 +10,16 @@ RSpec.describe Security::Finding do
describe 'validations' do describe 'validations' do
it { is_expected.to validate_presence_of(:project_fingerprint) } it { is_expected.to validate_presence_of(:project_fingerprint) }
it { is_expected.to validate_presence_of(:position) }
it { is_expected.to validate_length_of(:project_fingerprint).is_at_most(40) } it { is_expected.to validate_length_of(:project_fingerprint).is_at_most(40) }
end end
describe '.by_project_fingerprint' do describe '.by_position' do
let!(:finding_1) { create(:security_finding) } let!(:finding_1) { create(:security_finding, position: 0) }
let!(:finding_2) { create(:security_finding) } let!(:finding_2) { create(:security_finding, position: 1) }
let(:expected_findings) { [finding_1] } let(:expected_findings) { [finding_1] }
subject { described_class.by_project_fingerprint(finding_1.project_fingerprint) } subject { described_class.by_position(finding_1.position) }
it { is_expected.to match_array(expected_findings) } it { is_expected.to match_array(expected_findings) }
end end
......
...@@ -5,12 +5,13 @@ require 'spec_helper' ...@@ -5,12 +5,13 @@ require 'spec_helper'
RSpec.describe Security::StoreFindingsMetadataService do RSpec.describe Security::StoreFindingsMetadataService do
let_it_be(:security_scan) { create(:security_scan) } let_it_be(:security_scan) { create(:security_scan) }
let_it_be(:project) { security_scan.project } let_it_be(:project) { security_scan.project }
let_it_be(:security_finding) { build(:ci_reports_security_finding) } let_it_be(:security_finding_1) { build(:ci_reports_security_finding) }
let_it_be(:security_finding_2) { build(:ci_reports_security_finding) }
let_it_be(:security_scanner) { build(:ci_reports_security_scanner) } let_it_be(:security_scanner) { build(:ci_reports_security_scanner) }
let_it_be(:report) do let_it_be(:report) do
build( build(
:ci_reports_security_report, :ci_reports_security_report,
findings: [security_finding], findings: [security_finding_1, security_finding_2],
scanners: [security_scanner] scanners: [security_scanner]
) )
end end
...@@ -36,10 +37,12 @@ RSpec.describe Security::StoreFindingsMetadataService do ...@@ -36,10 +37,12 @@ RSpec.describe Security::StoreFindingsMetadataService do
end end
it 'creates the security finding entries in database' do it 'creates the security finding entries in database' do
expect { store_findings }.to change { security_scan.findings.count }.by(1) expect { store_findings }.to change { security_scan.findings.count }.by(2)
.and change { security_scan.findings.last&.severity }.to(security_finding.severity.to_s) .and change { security_scan.findings.first&.severity }.to(security_finding_1.severity.to_s)
.and change { security_scan.findings.last&.confidence }.to(security_finding.confidence.to_s) .and change { security_scan.findings.first&.confidence }.to(security_finding_1.confidence.to_s)
.and change { security_scan.findings.last&.project_fingerprint }.to(security_finding.project_fingerprint) .and change { security_scan.findings.first&.project_fingerprint }.to(security_finding_1.project_fingerprint)
.and change { security_scan.findings.first&.position }.to(0)
.and change { security_scan.findings.last&.position }.to(1)
end end
context 'when the scanners already exist in the database' do context 'when the scanners already exist in the database' do
......
...@@ -49,16 +49,16 @@ RSpec.describe Security::StoreScanService do ...@@ -49,16 +49,16 @@ RSpec.describe Security::StoreScanService do
context 'when the security scan already exists for the artifact' do context 'when the security scan already exists for the artifact' do
let_it_be(:security_scan) { create(:security_scan, build: artifact.job, scan_type: :sast) } let_it_be(:security_scan) { create(:security_scan, build: artifact.job, scan_type: :sast) }
let_it_be(:duplicated_security_finding) do let_it_be(:unique_security_finding) do
create(:security_finding, create(:security_finding,
scan: security_scan, scan: security_scan,
project_fingerprint: 'd533c3a12403b6c6033a50b53f9c73f894a40fc6') position: 0)
end end
let_it_be(:unique_security_finding) do let_it_be(:duplicated_security_finding) do
create(:security_finding, create(:security_finding,
scan: security_scan, scan: security_scan,
project_fingerprint: 'b9c0d1cdc7cb9c180ebb6981abbddc2df0172509') position: 5)
end end
it 'does not create a new security scan' do it 'does not create a new security scan' do
...@@ -89,12 +89,12 @@ RSpec.describe Security::StoreScanService do ...@@ -89,12 +89,12 @@ RSpec.describe Security::StoreScanService do
end end
context 'when the security scan does not exist for the artifact' do context 'when the security scan does not exist for the artifact' do
let(:duplicated_finding_attribute) do let(:unique_finding_attribute) do
-> { Security::Finding.by_project_fingerprint('d533c3a12403b6c6033a50b53f9c73f894a40fc6').first&.deduplicated } -> { Security::Finding.by_position(0).first&.deduplicated }
end end
let(:unique_finding_attribute) do let(:duplicated_finding_attribute) do
-> { Security::Finding.by_project_fingerprint('b9c0d1cdc7cb9c180ebb6981abbddc2df0172509').first&.deduplicated } -> { Security::Finding.by_position(5).first&.deduplicated }
end end
before do before do
......
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