Commit ed95f14b authored by Cyrine Gamoudi's avatar Cyrine Gamoudi Committed by Saikat Sarkar

Moving VulnerabilityReportsComparer to CE [RUN AS-IF-FOSS]

parent fc2cb5d4
...@@ -2,34 +2,11 @@ ...@@ -2,34 +2,11 @@
require 'spec_helper' require 'spec_helper'
# rubocop: disable RSpec/MultipleMemoizedHelpers
RSpec.describe Security::MergeReportsService, '#execute' do RSpec.describe Security::MergeReportsService, '#execute' do
let(:scanner_1) { build(:ci_reports_security_scanner, external_id: 'scanner-1', name: 'Scanner 1') }
let(:scanner_2) { build(:ci_reports_security_scanner, external_id: 'scanner-2', name: 'Scanner 2') } let(:scanner_2) { build(:ci_reports_security_scanner, external_id: 'scanner-2', name: 'Scanner 2') }
let(:scanner_3) { build(:ci_reports_security_scanner, external_id: 'scanner-3', name: 'Scanner 3') } let(:identifier_cwe) { build(:ci_reports_security_identifier, external_id: '789', external_type: 'cwe') }
let(:identifier_1_primary) { build(:ci_reports_security_identifier, external_id: 'VULN-1', external_type: 'scanner-1') }
let(:identifier_1_cve) { build(:ci_reports_security_identifier, external_id: 'CVE-2019-123', external_type: 'cve') }
let(:identifier_2_primary) { build(:ci_reports_security_identifier, external_id: 'VULN-2', external_type: 'scanner-2') } let(:identifier_2_primary) { build(:ci_reports_security_identifier, external_id: 'VULN-2', external_type: 'scanner-2') }
let(:identifier_2_cve) { build(:ci_reports_security_identifier, external_id: 'CVE-2019-456', external_type: 'cve') } let(:identifier_2_cve) { build(:ci_reports_security_identifier, external_id: 'CVE-2019-456', external_type: 'cve') }
let(:identifier_cwe) { build(:ci_reports_security_identifier, external_id: '789', external_type: 'cwe') }
let(:identifier_wasc) { build(:ci_reports_security_identifier, external_id: '13', external_type: 'wasc') }
let(:finding_id_1) do
build(:ci_reports_security_finding,
identifiers: [identifier_1_primary, identifier_1_cve],
scanner: scanner_1,
severity: :low
)
end
let(:finding_id_1_extra) do
build(:ci_reports_security_finding,
identifiers: [identifier_1_primary, identifier_1_cve],
scanner: scanner_1,
severity: :low
)
end
let(:finding_id_2_loc_1) do let(:finding_id_2_loc_1) do
build(:ci_reports_security_finding, build(:ci_reports_security_finding,
...@@ -40,164 +17,6 @@ RSpec.describe Security::MergeReportsService, '#execute' do ...@@ -40,164 +17,6 @@ RSpec.describe Security::MergeReportsService, '#execute' do
) )
end end
let(:finding_id_2_loc_1_extra) do
build(:ci_reports_security_finding,
identifiers: [identifier_2_primary, identifier_2_cve],
location: build(:ci_reports_security_locations_sast, start_line: 32, end_line: 34),
scanner: scanner_2,
severity: :medium
)
end
let(:finding_id_2_loc_2) do
build(:ci_reports_security_finding,
identifiers: [identifier_2_primary, identifier_2_cve],
location: build(:ci_reports_security_locations_sast, start_line: 42, end_line: 44),
scanner: scanner_2,
severity: :medium
)
end
let(:finding_cwe_1) do
build(:ci_reports_security_finding,
identifiers: [identifier_cwe],
scanner: scanner_3,
severity: :high
)
end
let(:finding_cwe_2) do
build(:ci_reports_security_finding,
identifiers: [identifier_cwe],
scanner: scanner_1,
severity: :critical
)
end
let(:finding_wasc_1) do
build(:ci_reports_security_finding,
identifiers: [identifier_wasc],
scanner: scanner_1,
severity: :medium
)
end
let(:finding_wasc_2) do
build(:ci_reports_security_finding,
identifiers: [identifier_wasc],
scanner: scanner_2,
severity: :critical
)
end
let(:report_1_findings) { [finding_id_1, finding_id_2_loc_1, finding_id_2_loc_1_extra, finding_cwe_2, finding_wasc_1] }
let(:scanned_resource) do
::Gitlab::Ci::Reports::Security::ScannedResource.new(URI.parse('example.com'), 'GET')
end
let(:scanned_resource_1) do
::Gitlab::Ci::Reports::Security::ScannedResource.new(URI.parse('example.com'), 'POST')
end
let(:scanned_resource_2) do
::Gitlab::Ci::Reports::Security::ScannedResource.new(URI.parse('example.com/2'), 'GET')
end
let(:scanned_resource_3) do
::Gitlab::Ci::Reports::Security::ScannedResource.new(URI.parse('example.com/3'), 'GET')
end
let(:report_1) do
build(
:ci_reports_security_report,
scanners: [scanner_1, scanner_2],
findings: report_1_findings,
identifiers: report_1_findings.flat_map(&:identifiers),
scanned_resources: [scanned_resource, scanned_resource_1, scanned_resource_2]
)
end
let(:report_2_findings) { [finding_id_2_loc_2, finding_wasc_2] }
let(:report_2) do
build(
:ci_reports_security_report,
scanners: [scanner_2],
findings: report_2_findings,
identifiers: finding_id_2_loc_2.identifiers,
scanned_resources: [scanned_resource, scanned_resource_1, scanned_resource_3]
)
end
let(:report_3_findings) { [finding_id_1_extra, finding_cwe_1] }
let(:report_3) do
build(
:ci_reports_security_report,
scanners: [scanner_1, scanner_3],
findings: report_3_findings,
identifiers: report_3_findings.flat_map(&:identifiers)
)
end
let(:merge_service) { described_class.new(report_1, report_2, report_3) }
subject(:merged_report) { merge_service.execute }
describe 'errors on target report' do
subject { merged_report.errors }
before do
report_1.add_error('foo', 'bar')
report_2.add_error('zoo', 'baz')
end
it { is_expected.to eq([{ type: 'foo', message: 'bar' }, { type: 'zoo', message: 'baz' }]) }
end
it 'copies scanners into target report and eliminates duplicates' do
expect(merged_report.scanners.values).to contain_exactly(scanner_1, scanner_2, scanner_3)
end
it 'copies identifiers into target report and eliminates duplicates' do
expect(merged_report.identifiers.values).to(
contain_exactly(
identifier_1_primary,
identifier_1_cve,
identifier_2_primary,
identifier_2_cve,
identifier_cwe,
identifier_wasc
)
)
end
it 'deduplicates (except cwe and wasc) and sorts the vulnerabilities by severity (desc) then by compare key' do
expect(merged_report.findings).to(
eq([
finding_cwe_2,
finding_wasc_2,
finding_cwe_1,
finding_id_2_loc_2,
finding_id_2_loc_1,
finding_wasc_1,
finding_id_1
])
)
end
it 'deduplicates scanned resources' do
expect(merged_report.scanned_resources).to(
eq([
scanned_resource,
scanned_resource_1,
scanned_resource_2,
scanned_resource_3
])
)
end
context 'ordering reports for dependency scanning analyzers' do context 'ordering reports for dependency scanning analyzers' do
let(:gemnasium_scanner) { build(:ci_reports_security_scanner, external_id: 'gemnasium', name: 'gemnasium') } let(:gemnasium_scanner) { build(:ci_reports_security_scanner, external_id: 'gemnasium', name: 'gemnasium') }
let(:retire_js_scaner) { build(:ci_reports_security_scanner, external_id: 'retire.js', name: 'Retire.js') } let(:retire_js_scaner) { build(:ci_reports_security_scanner, external_id: 'retire.js', name: 'Retire.js') }
...@@ -299,64 +118,4 @@ RSpec.describe Security::MergeReportsService, '#execute' do ...@@ -299,64 +118,4 @@ RSpec.describe Security::MergeReportsService, '#execute' do
end end
end end
end end
context 'ordering reports for sast analyzers' do
let(:bandit_scanner) { build(:ci_reports_security_scanner, external_id: 'bandit', name: 'Bandit') }
let(:semgrep_scanner) { build(:ci_reports_security_scanner, external_id: 'semgrep', name: 'Semgrep') }
let(:identifier_bandit) { build(:ci_reports_security_identifier, external_id: 'B403', external_type: 'bandit_test_id') }
let(:identifier_cve) { build(:ci_reports_security_identifier, external_id: 'CVE-2019-123', external_type: 'cve') }
let(:identifier_semgrep) { build(:ci_reports_security_identifier, external_id: 'rules.bandit.B105', external_type: 'semgrep_id') }
let(:finding_id_1) { build(:ci_reports_security_finding, identifiers: [identifier_bandit, identifier_cve], scanner: bandit_scanner, report_type: :sast) }
let(:finding_id_2) { build(:ci_reports_security_finding, identifiers: [identifier_cve], scanner: semgrep_scanner, report_type: :sast) }
let(:finding_id_3) { build(:ci_reports_security_finding, identifiers: [identifier_semgrep], scanner: semgrep_scanner, report_type: :sast ) }
let(:bandit_report) do
build( :ci_reports_security_report,
type: :sast,
scanners: [bandit_scanner],
findings: [finding_id_1],
identifiers: finding_id_1.identifiers
)
end
let(:semgrep_report) do
build(
:ci_reports_security_report,
type: :sast,
scanners: [semgrep_scanner],
findings: [finding_id_2, finding_id_3],
identifiers: finding_id_2.identifiers + finding_id_3.identifiers
)
end
let(:custom_analyzer_report) do
build(
:ci_reports_security_report,
type: :sast,
scanners: [scanner_2],
findings: [finding_id_2_loc_1],
identifiers: finding_id_2_loc_1.identifiers
)
end
context 'when reports are gathered in an unprioritized order' do
subject(:sast_merged_report) { described_class.new(semgrep_report, bandit_report).execute }
specify { expect(sast_merged_report.scanners.values).to eql([bandit_scanner, semgrep_scanner]) }
specify { expect(sast_merged_report.findings.count).to eq(2) }
specify { expect(sast_merged_report.findings.first.identifiers).to eql([identifier_bandit, identifier_cve]) }
specify { expect(sast_merged_report.findings.last.identifiers).to contain_exactly(identifier_semgrep) }
end
context 'when a custom analyzer is completed before the known analyzers' do
subject(:sast_merged_report) { described_class.new(custom_analyzer_report, semgrep_report, bandit_report).execute }
specify { expect(sast_merged_report.scanners.values).to eql([bandit_scanner, semgrep_scanner, scanner_2]) }
specify { expect(sast_merged_report.findings.count).to eq(3) }
specify { expect(sast_merged_report.findings.last.identifiers).to match_array(finding_id_2_loc_1.identifiers) }
end
end
end end
# rubocop: enable RSpec/MultipleMemoizedHelpers
...@@ -3,21 +3,22 @@ ...@@ -3,21 +3,22 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Gitlab::Ci::Reports::Security::VulnerabilityReportsComparer do RSpec.describe Gitlab::Ci::Reports::Security::VulnerabilityReportsComparer do
let(:identifier) { build(:vulnerabilities_identifier) } let(:identifier) { build(:ci_reports_security_identifier) }
let_it_be(:project) { create(:project, :repository) } let_it_be(:project) { create(:project, :repository) }
let(:vulnerability_params) { { project: project, report_type: :sast, identifiers: [identifier], confidence: ::Enums::Vulnerability.confidence_levels[:high], severity: ::Enums::Vulnerability.severity_levels[:critical] } } let(:location_param) { build(:ci_reports_security_locations_sast, :dynamic) }
let(:base_vulnerability) { build(:vulnerabilities_finding, location_fingerprint: '123', **vulnerability_params) } let(:vulnerability_params) { vuln_params(project.id, [identifier], confidence: :low, severity: :critical) }
let(:base_report) { build(:ci_reports_security_aggregated_reports, findings: [base_vulnerability])} let(:base_vulnerability) { build(:ci_reports_security_finding, location: location_param, **vulnerability_params) }
let(:base_report) { build(:ci_reports_security_aggregated_reports, findings: [base_vulnerability]) }
let(:head_vulnerability) { build(:vulnerabilities_finding, location_fingerprint: base_vulnerability.location_fingerprint, uuid: base_vulnerability.uuid, **vulnerability_params) } let(:head_vulnerability) { build(:ci_reports_security_finding, location: location_param, uuid: base_vulnerability.uuid, **vulnerability_params) }
let(:head_report) { build(:ci_reports_security_aggregated_reports, findings: [head_vulnerability])} let(:head_report) { build(:ci_reports_security_aggregated_reports, findings: [head_vulnerability]) }
shared_context 'comparing reports' do shared_context 'comparing reports' do
let(:vul_params) { { project: project, report_type: :sast, identifiers: [identifier] } } let(:vul_params) { vuln_params(project.id, [identifier]) }
let(:base_vulnerability) { build(:vulnerabilities_finding, location_fingerprint: 'A', **vul_params) } let(:base_vulnerability) { build(:ci_reports_security_finding, :dynamic, **vul_params) }
let(:head_vulnerability) { build(:vulnerabilities_finding, location_fingerprint: 'B', **vul_params) } let(:head_vulnerability) { build(:ci_reports_security_finding, :dynamic, **vul_params) }
let(:head_vul_findings) { [head_vulnerability, vuln] } let(:head_vul_findings) { [head_vulnerability, vuln] }
end end
...@@ -27,15 +28,13 @@ RSpec.describe Gitlab::Ci::Reports::Security::VulnerabilityReportsComparer do ...@@ -27,15 +28,13 @@ RSpec.describe Gitlab::Ci::Reports::Security::VulnerabilityReportsComparer do
with_them do with_them do
before do before do
allow(base_vulnerability).to receive(:location).and_return({})
allow(head_vulnerability).to receive(:location).and_return({})
stub_feature_flags(vulnerability_finding_tracking_signatures: vulnerability_finding_tracking_signatures_enabled) stub_feature_flags(vulnerability_finding_tracking_signatures: vulnerability_finding_tracking_signatures_enabled)
stub_licensed_features(vulnerability_finding_signatures: vulnerability_finding_tracking_signatures_enabled) stub_licensed_features(vulnerability_finding_signatures: vulnerability_finding_tracking_signatures_enabled)
end end
describe '#base_report_out_of_date' do describe '#base_report_out_of_date' do
context 'no base report' do context 'no base report' do
let(:base_report) { build(:ci_reports_security_aggregated_reports, reports: [], findings: [])} let(:base_report) { build(:ci_reports_security_aggregated_reports, reports: [], findings: []) }
it 'is not out of date' do it 'is not out of date' do
expect(subject.base_report_out_of_date).to be false expect(subject.base_report_out_of_date).to be false
...@@ -44,7 +43,7 @@ RSpec.describe Gitlab::Ci::Reports::Security::VulnerabilityReportsComparer do ...@@ -44,7 +43,7 @@ RSpec.describe Gitlab::Ci::Reports::Security::VulnerabilityReportsComparer do
context 'base report older than one week' do context 'base report older than one week' do
let(:report) { build(:ci_reports_security_report, created_at: 1.week.ago - 60.seconds) } let(:report) { build(:ci_reports_security_report, created_at: 1.week.ago - 60.seconds) }
let(:base_report) { build(:ci_reports_security_aggregated_reports, reports: [report])} let(:base_report) { build(:ci_reports_security_aggregated_reports, reports: [report]) }
it 'is not out of date' do it 'is not out of date' do
expect(subject.base_report_out_of_date).to be true expect(subject.base_report_out_of_date).to be true
...@@ -53,7 +52,7 @@ RSpec.describe Gitlab::Ci::Reports::Security::VulnerabilityReportsComparer do ...@@ -53,7 +52,7 @@ RSpec.describe Gitlab::Ci::Reports::Security::VulnerabilityReportsComparer do
context 'base report less than one week old' do context 'base report less than one week old' do
let(:report) { build(:ci_reports_security_report, created_at: 1.week.ago + 60.seconds) } let(:report) { build(:ci_reports_security_report, created_at: 1.week.ago + 60.seconds) }
let(:base_report) { build(:ci_reports_security_aggregated_reports, reports: [report])} let(:base_report) { build(:ci_reports_security_aggregated_reports, reports: [report]) }
it 'is not out of date' do it 'is not out of date' do
expect(subject.base_report_out_of_date).to be false expect(subject.base_report_out_of_date).to be false
...@@ -62,12 +61,13 @@ RSpec.describe Gitlab::Ci::Reports::Security::VulnerabilityReportsComparer do ...@@ -62,12 +61,13 @@ RSpec.describe Gitlab::Ci::Reports::Security::VulnerabilityReportsComparer do
end end
describe '#added' do describe '#added' do
let(:vul_params) { { project: project, report_type: :sast, identifiers: [identifier], location_fingerprint: '888', confidence: ::Enums::Vulnerability.confidence_levels[:high] } } let(:new_location) {build(:ci_reports_security_locations_sast, :dynamic) }
let(:vuln) { build(:vulnerabilities_finding, severity: Enums::Vulnerability.severity_levels[:critical], **vul_params) } let(:vul_params) { vuln_params(project.id, [identifier], confidence: :high) }
let(:low_vuln) { build(:vulnerabilities_finding, severity: Enums::Vulnerability.severity_levels[:low], **vul_params) } let(:vuln) { build(:ci_reports_security_finding, severity: Enums::Vulnerability.severity_levels[:critical], location: new_location, **vul_params) }
let(:low_vuln) { build(:ci_reports_security_finding, severity: Enums::Vulnerability.severity_levels[:low], location: new_location, **vul_params) }
context 'with new vulnerability' do context 'with new vulnerability' do
let(:head_report) { build(:ci_reports_security_aggregated_reports, findings: [head_vulnerability, vuln])} let(:head_report) { build(:ci_reports_security_aggregated_reports, findings: [head_vulnerability, vuln]) }
it 'points to source tree' do it 'points to source tree' do
expect(subject.added).to eq([vuln]) expect(subject.added).to eq([vuln])
...@@ -77,7 +77,7 @@ RSpec.describe Gitlab::Ci::Reports::Security::VulnerabilityReportsComparer do ...@@ -77,7 +77,7 @@ RSpec.describe Gitlab::Ci::Reports::Security::VulnerabilityReportsComparer do
context 'when comparing reports with different fingerprints' do context 'when comparing reports with different fingerprints' do
include_context 'comparing reports' include_context 'comparing reports'
let(:head_report) { build(:ci_reports_security_aggregated_reports, findings: head_vul_findings)} let(:head_report) { build(:ci_reports_security_aggregated_reports, findings: head_vul_findings) }
it 'does not find any overlap' do it 'does not find any overlap' do
expect(subject.added).to eq(head_vul_findings) expect(subject.added).to eq(head_vul_findings)
...@@ -85,7 +85,7 @@ RSpec.describe Gitlab::Ci::Reports::Security::VulnerabilityReportsComparer do ...@@ -85,7 +85,7 @@ RSpec.describe Gitlab::Ci::Reports::Security::VulnerabilityReportsComparer do
end end
context 'order' do context 'order' do
let(:head_report) { build(:ci_reports_security_aggregated_reports, findings: [head_vulnerability, vuln, low_vuln])} let(:head_report) { build(:ci_reports_security_aggregated_reports, findings: [head_vulnerability, vuln, low_vuln]) }
it 'does not change' do it 'does not change' do
expect(subject.added).to eq([vuln, low_vuln]) expect(subject.added).to eq([vuln, low_vuln])
...@@ -94,12 +94,12 @@ RSpec.describe Gitlab::Ci::Reports::Security::VulnerabilityReportsComparer do ...@@ -94,12 +94,12 @@ RSpec.describe Gitlab::Ci::Reports::Security::VulnerabilityReportsComparer do
end end
describe '#fixed' do describe '#fixed' do
let(:vul_params) { { project: project, report_type: :sast, identifiers: [identifier], location_fingerprint: '888' } } let(:vul_params) { vuln_params(project.id, [identifier]) }
let(:vuln) { build(:vulnerabilities_finding, **vul_params ) } let(:vuln) { build(:ci_reports_security_finding, :dynamic, **vul_params ) }
let(:medium_vuln) { build(:vulnerabilities_finding, confidence: ::Enums::Vulnerability.confidence_levels[:high], severity: Enums::Vulnerability.severity_levels[:medium], uuid: vuln.uuid, **vul_params) } let(:medium_vuln) { build(:ci_reports_security_finding, confidence: ::Enums::Vulnerability.confidence_levels[:high], severity: Enums::Vulnerability.severity_levels[:medium], uuid: vuln.uuid, **vul_params) }
context 'with fixed vulnerability' do context 'with fixed vulnerability' do
let(:base_report) { build(:ci_reports_security_aggregated_reports, findings: [base_vulnerability, vuln])} let(:base_report) { build(:ci_reports_security_aggregated_reports, findings: [base_vulnerability, vuln]) }
it 'points to base tree' do it 'points to base tree' do
expect(subject.fixed).to eq([vuln]) expect(subject.fixed).to eq([vuln])
...@@ -109,7 +109,7 @@ RSpec.describe Gitlab::Ci::Reports::Security::VulnerabilityReportsComparer do ...@@ -109,7 +109,7 @@ RSpec.describe Gitlab::Ci::Reports::Security::VulnerabilityReportsComparer do
context 'when comparing reports with different fingerprints' do context 'when comparing reports with different fingerprints' do
include_context 'comparing reports' include_context 'comparing reports'
let(:base_report) { build(:ci_reports_security_aggregated_reports, findings: [base_vulnerability, vuln])} let(:base_report) { build(:ci_reports_security_aggregated_reports, findings: [base_vulnerability, vuln]) }
it 'does not find any overlap' do it 'does not find any overlap' do
expect(subject.fixed).to eq([base_vulnerability, vuln]) expect(subject.fixed).to eq([base_vulnerability, vuln])
...@@ -118,7 +118,7 @@ RSpec.describe Gitlab::Ci::Reports::Security::VulnerabilityReportsComparer do ...@@ -118,7 +118,7 @@ RSpec.describe Gitlab::Ci::Reports::Security::VulnerabilityReportsComparer do
context 'order' do context 'order' do
let(:vul_findings) { [vuln, medium_vuln] } let(:vul_findings) { [vuln, medium_vuln] }
let(:base_report) { build(:ci_reports_security_aggregated_reports, findings: [*vul_findings, base_vulnerability])} let(:base_report) { build(:ci_reports_security_aggregated_reports, findings: [*vul_findings, base_vulnerability]) }
it 'does not change' do it 'does not change' do
expect(subject.fixed).to eq(vul_findings) expect(subject.fixed).to eq(vul_findings)
...@@ -127,7 +127,7 @@ RSpec.describe Gitlab::Ci::Reports::Security::VulnerabilityReportsComparer do ...@@ -127,7 +127,7 @@ RSpec.describe Gitlab::Ci::Reports::Security::VulnerabilityReportsComparer do
end end
describe 'with empty vulnerabilities' do describe 'with empty vulnerabilities' do
let(:empty_report) { build(:ci_reports_security_aggregated_reports, reports: [], findings: [])} let(:empty_report) { build(:ci_reports_security_aggregated_reports, reports: [], findings: []) }
it 'returns empty array when reports are not present' do it 'returns empty array when reports are not present' do
comparer = described_class.new(project, empty_report, empty_report) comparer = described_class.new(project, empty_report, empty_report)
...@@ -151,4 +151,14 @@ RSpec.describe Gitlab::Ci::Reports::Security::VulnerabilityReportsComparer do ...@@ -151,4 +151,14 @@ RSpec.describe Gitlab::Ci::Reports::Security::VulnerabilityReportsComparer do
end end
end end
end end
def vuln_params(project_id, identifiers, confidence: :high, severity: :critical)
{
project_id: project_id,
report_type: :sast,
identifiers: identifiers,
confidence: ::Enums::Vulnerability.confidence_levels[confidence],
severity: ::Enums::Vulnerability.severity_levels[severity]
}
end
end end
# frozen_string_literal: true
require 'spec_helper'
# rubocop: disable RSpec/MultipleMemoizedHelpers
RSpec.describe Security::MergeReportsService, '#execute' do
let(:scanner_1) { build(:ci_reports_security_scanner, external_id: 'scanner-1', name: 'Scanner 1') }
let(:scanner_2) { build(:ci_reports_security_scanner, external_id: 'scanner-2', name: 'Scanner 2') }
let(:scanner_3) { build(:ci_reports_security_scanner, external_id: 'scanner-3', name: 'Scanner 3') }
let(:identifier_1_primary) { build(:ci_reports_security_identifier, external_id: 'VULN-1', external_type: 'scanner-1') }
let(:identifier_1_cve) { build(:ci_reports_security_identifier, external_id: 'CVE-2019-123', external_type: 'cve') }
let(:identifier_2_primary) { build(:ci_reports_security_identifier, external_id: 'VULN-2', external_type: 'scanner-2') }
let(:identifier_2_cve) { build(:ci_reports_security_identifier, external_id: 'CVE-2019-456', external_type: 'cve') }
let(:identifier_cwe) { build(:ci_reports_security_identifier, external_id: '789', external_type: 'cwe') }
let(:identifier_wasc) { build(:ci_reports_security_identifier, external_id: '13', external_type: 'wasc') }
let(:finding_id_1) do
build(:ci_reports_security_finding,
identifiers: [identifier_1_primary, identifier_1_cve],
scanner: scanner_1,
severity: :low
)
end
let(:finding_id_1_extra) do
build(:ci_reports_security_finding,
identifiers: [identifier_1_primary, identifier_1_cve],
scanner: scanner_1,
severity: :low
)
end
let(:finding_id_2_loc_1) do
build(:ci_reports_security_finding,
identifiers: [identifier_2_primary, identifier_2_cve],
location: build(:ci_reports_security_locations_sast, start_line: 32, end_line: 34),
scanner: scanner_2,
severity: :medium
)
end
let(:finding_id_2_loc_1_extra) do
build(:ci_reports_security_finding,
identifiers: [identifier_2_primary, identifier_2_cve],
location: build(:ci_reports_security_locations_sast, start_line: 32, end_line: 34),
scanner: scanner_2,
severity: :medium
)
end
let(:finding_id_2_loc_2) do
build(:ci_reports_security_finding,
identifiers: [identifier_2_primary, identifier_2_cve],
location: build(:ci_reports_security_locations_sast, start_line: 42, end_line: 44),
scanner: scanner_2,
severity: :medium
)
end
let(:finding_cwe_1) do
build(:ci_reports_security_finding,
identifiers: [identifier_cwe],
scanner: scanner_3,
severity: :high
)
end
let(:finding_cwe_2) do
build(:ci_reports_security_finding,
identifiers: [identifier_cwe],
scanner: scanner_1,
severity: :critical
)
end
let(:finding_wasc_1) do
build(:ci_reports_security_finding,
identifiers: [identifier_wasc],
scanner: scanner_1,
severity: :medium
)
end
let(:finding_wasc_2) do
build(:ci_reports_security_finding,
identifiers: [identifier_wasc],
scanner: scanner_2,
severity: :critical
)
end
let(:report_1_findings) { [finding_id_1, finding_id_2_loc_1, finding_id_2_loc_1_extra, finding_cwe_2, finding_wasc_1] }
let(:scanned_resource) do
::Gitlab::Ci::Reports::Security::ScannedResource.new(URI.parse('example.com'), 'GET')
end
let(:scanned_resource_1) do
::Gitlab::Ci::Reports::Security::ScannedResource.new(URI.parse('example.com'), 'POST')
end
let(:scanned_resource_2) do
::Gitlab::Ci::Reports::Security::ScannedResource.new(URI.parse('example.com/2'), 'GET')
end
let(:scanned_resource_3) do
::Gitlab::Ci::Reports::Security::ScannedResource.new(URI.parse('example.com/3'), 'GET')
end
let(:report_1) do
build(
:ci_reports_security_report,
scanners: [scanner_1, scanner_2],
findings: report_1_findings,
identifiers: report_1_findings.flat_map(&:identifiers),
scanned_resources: [scanned_resource, scanned_resource_1, scanned_resource_2]
)
end
let(:report_2_findings) { [finding_id_2_loc_2, finding_wasc_2] }
let(:report_2) do
build(
:ci_reports_security_report,
scanners: [scanner_2],
findings: report_2_findings,
identifiers: finding_id_2_loc_2.identifiers,
scanned_resources: [scanned_resource, scanned_resource_1, scanned_resource_3]
)
end
let(:report_3_findings) { [finding_id_1_extra, finding_cwe_1] }
let(:report_3) do
build(
:ci_reports_security_report,
scanners: [scanner_1, scanner_3],
findings: report_3_findings,
identifiers: report_3_findings.flat_map(&:identifiers)
)
end
let(:merge_service) { described_class.new(report_1, report_2, report_3) }
subject(:merged_report) { merge_service.execute }
describe 'errors on target report' do
subject { merged_report.errors }
before do
report_1.add_error('foo', 'bar')
report_2.add_error('zoo', 'baz')
end
it { is_expected.to eq([{ type: 'foo', message: 'bar' }, { type: 'zoo', message: 'baz' }]) }
end
it 'copies scanners into target report and eliminates duplicates' do
expect(merged_report.scanners.values).to contain_exactly(scanner_1, scanner_2, scanner_3)
end
it 'copies identifiers into target report and eliminates duplicates' do
expect(merged_report.identifiers.values).to(
contain_exactly(
identifier_1_primary,
identifier_1_cve,
identifier_2_primary,
identifier_2_cve,
identifier_cwe,
identifier_wasc
)
)
end
it 'deduplicates (except cwe and wasc) and sorts the vulnerabilities by severity (desc) then by compare key' do
expect(merged_report.findings).to(
eq([
finding_cwe_2,
finding_wasc_2,
finding_cwe_1,
finding_id_2_loc_2,
finding_id_2_loc_1,
finding_wasc_1,
finding_id_1
])
)
end
it 'deduplicates scanned resources' do
expect(merged_report.scanned_resources).to(
eq([
scanned_resource,
scanned_resource_1,
scanned_resource_2,
scanned_resource_3
])
)
end
context 'ordering reports for sast analyzers' do
let(:bandit_scanner) { build(:ci_reports_security_scanner, external_id: 'bandit', name: 'Bandit') }
let(:semgrep_scanner) { build(:ci_reports_security_scanner, external_id: 'semgrep', name: 'Semgrep') }
let(:identifier_bandit) { build(:ci_reports_security_identifier, external_id: 'B403', external_type: 'bandit_test_id') }
let(:identifier_cve) { build(:ci_reports_security_identifier, external_id: 'CVE-2019-123', external_type: 'cve') }
let(:identifier_semgrep) { build(:ci_reports_security_identifier, external_id: 'rules.bandit.B105', external_type: 'semgrep_id') }
let(:finding_id_1) { build(:ci_reports_security_finding, identifiers: [identifier_bandit, identifier_cve], scanner: bandit_scanner, report_type: :sast) }
let(:finding_id_2) { build(:ci_reports_security_finding, identifiers: [identifier_cve], scanner: semgrep_scanner, report_type: :sast) }
let(:finding_id_3) { build(:ci_reports_security_finding, identifiers: [identifier_semgrep], scanner: semgrep_scanner, report_type: :sast ) }
let(:bandit_report) do
build( :ci_reports_security_report,
type: :sast,
scanners: [bandit_scanner],
findings: [finding_id_1],
identifiers: finding_id_1.identifiers
)
end
let(:semgrep_report) do
build(
:ci_reports_security_report,
type: :sast,
scanners: [semgrep_scanner],
findings: [finding_id_2, finding_id_3],
identifiers: finding_id_2.identifiers + finding_id_3.identifiers
)
end
let(:custom_analyzer_report) do
build(
:ci_reports_security_report,
type: :sast,
scanners: [scanner_2],
findings: [finding_id_2_loc_1],
identifiers: finding_id_2_loc_1.identifiers
)
end
context 'when reports are gathered in an unprioritized order' do
subject(:sast_merged_report) { described_class.new(semgrep_report, bandit_report).execute }
specify { expect(sast_merged_report.scanners.values).to eql([bandit_scanner, semgrep_scanner]) }
specify { expect(sast_merged_report.findings.count).to eq(2) }
specify { expect(sast_merged_report.findings.first.identifiers).to eql([identifier_bandit, identifier_cve]) }
specify { expect(sast_merged_report.findings.last.identifiers).to contain_exactly(identifier_semgrep) }
end
context 'when a custom analyzer is completed before the known analyzers' do
subject(:sast_merged_report) { described_class.new(custom_analyzer_report, semgrep_report, bandit_report).execute }
specify { expect(sast_merged_report.scanners.values).to eql([bandit_scanner, semgrep_scanner, scanner_2]) }
specify { expect(sast_merged_report.findings.count).to eq(3) }
specify { expect(sast_merged_report.findings.last.identifiers).to match_array(finding_id_2_loc_1.identifiers) }
end
end
end
# rubocop: enable RSpec/MultipleMemoizedHelpers
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