Commit c287f790 authored by Mehmet Emin INAC's avatar Mehmet Emin INAC Committed by Rémy Coutable

Mark findings as deduplicated based on their position

It is possible to have more than one finding with the same project
fingerprint for a report artifact which breaks the logic of marking
findings as deduplicated based on project fingerprint values.

For this reason, we need to mark findings as deduplicated based on
their position within their reports.
parent 52a4487b
---
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