261 lines
9.1 KiB
Ruby
261 lines
9.1 KiB
Ruby
|
# 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
|