Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'spec/lib/gitlab/ci/reports')
-rw-r--r--spec/lib/gitlab/ci/reports/security/aggregated_report_spec.rb45
-rw-r--r--spec/lib/gitlab/ci/reports/security/finding_key_spec.rb41
-rw-r--r--spec/lib/gitlab/ci/reports/security/finding_signature_spec.rb59
-rw-r--r--spec/lib/gitlab/ci/reports/security/locations/sast_spec.rb21
-rw-r--r--spec/lib/gitlab/ci/reports/security/locations/secret_detection_spec.rb21
-rw-r--r--spec/lib/gitlab/ci/reports/security/report_spec.rb224
-rw-r--r--spec/lib/gitlab/ci/reports/security/reports_spec.rb113
-rw-r--r--spec/lib/gitlab/ci/reports/security/vulnerability_reports_comparer_spec.rb163
8 files changed, 687 insertions, 0 deletions
diff --git a/spec/lib/gitlab/ci/reports/security/aggregated_report_spec.rb b/spec/lib/gitlab/ci/reports/security/aggregated_report_spec.rb
new file mode 100644
index 00000000000..c56177a6453
--- /dev/null
+++ b/spec/lib/gitlab/ci/reports/security/aggregated_report_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Reports::Security::AggregatedReport do
+ subject { described_class.new(reports, findings) }
+
+ let(:reports) { build_list(:ci_reports_security_report, 1) }
+ let(:findings) { build_list(:ci_reports_security_finding, 1) }
+
+ describe '#created_at' do
+ context 'no reports' do
+ let(:reports) { [] }
+
+ it 'has no created date' do
+ expect(subject.created_at).to be_nil
+ end
+ end
+
+ context 'report with no created date' do
+ let(:reports) { build_list(:ci_reports_security_report, 1, created_at: nil) }
+
+ it 'has no created date' do
+ expect(subject.created_at).to be_nil
+ end
+ end
+
+ context 'has reports' do
+ let(:a_long_time_ago) { 2.months.ago }
+ let(:a_while_ago) { 2.weeks.ago }
+ let(:yesterday) { 1.day.ago }
+
+ let(:reports) do
+ [build(:ci_reports_security_report, created_at: a_while_ago),
+ build(:ci_reports_security_report, created_at: a_long_time_ago),
+ build(:ci_reports_security_report, created_at: nil),
+ build(:ci_reports_security_report, created_at: yesterday)]
+ end
+
+ it 'has oldest created date' do
+ expect(subject.created_at).to eq(a_long_time_ago)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/reports/security/finding_key_spec.rb b/spec/lib/gitlab/ci/reports/security/finding_key_spec.rb
new file mode 100644
index 00000000000..784c1183320
--- /dev/null
+++ b/spec/lib/gitlab/ci/reports/security/finding_key_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Reports::Security::FindingKey do
+ using RSpec::Parameterized::TableSyntax
+
+ describe '#==' do
+ where(:location_fp_1, :location_fp_2, :identifier_fp_1, :identifier_fp_2, :equals?) do
+ nil | 'different location fp' | 'identifier fp' | 'different identifier fp' | false
+ 'location fp' | nil | 'identifier fp' | 'different identifier fp' | false
+ 'location fp' | 'different location fp' | nil | 'different identifier fp' | false
+ 'location fp' | 'different location fp' | 'identifier fp' | nil | false
+ nil | nil | 'identifier fp' | 'identifier fp' | false
+ 'location fp' | 'location fp' | nil | nil | false
+ nil | nil | nil | nil | false
+ 'location fp' | 'different location fp' | 'identifier fp' | 'different identifier fp' | false
+ 'location fp' | 'different location fp' | 'identifier fp' | 'identifier fp' | false
+ 'location fp' | 'location fp' | 'identifier fp' | 'different identifier fp' | false
+ 'location fp' | 'location fp' | 'identifier fp' | 'identifier fp' | true
+ end
+
+ with_them do
+ let(:finding_key_1) do
+ build(:ci_reports_security_finding_key,
+ location_fingerprint: location_fp_1,
+ identifier_fingerprint: identifier_fp_1)
+ end
+
+ let(:finding_key_2) do
+ build(:ci_reports_security_finding_key,
+ location_fingerprint: location_fp_2,
+ identifier_fingerprint: identifier_fp_2)
+ end
+
+ subject { finding_key_1 == finding_key_2 }
+
+ it { is_expected.to be(equals?) }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/reports/security/finding_signature_spec.rb b/spec/lib/gitlab/ci/reports/security/finding_signature_spec.rb
new file mode 100644
index 00000000000..23e6b40a039
--- /dev/null
+++ b/spec/lib/gitlab/ci/reports/security/finding_signature_spec.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Reports::Security::FindingSignature do
+ subject { described_class.new(params.with_indifferent_access) }
+
+ let(:params) do
+ {
+ algorithm_type: 'hash',
+ signature_value: 'SIGNATURE'
+ }
+ end
+
+ describe '#initialize' do
+ context 'when a supported algorithm type is given' do
+ it 'allows itself to be created' do
+ expect(subject.algorithm_type).to eq(params[:algorithm_type])
+ expect(subject.signature_value).to eq(params[:signature_value])
+ end
+
+ describe '#valid?' do
+ it 'returns true' do
+ expect(subject.valid?).to eq(true)
+ end
+ end
+ end
+ end
+
+ describe '#valid?' do
+ context 'when supported algorithm_type is given' do
+ it 'is valid' do
+ expect(subject.valid?).to eq(true)
+ end
+ end
+
+ context 'when an unsupported algorithm_type is given' do
+ let(:params) do
+ {
+ algorithm_type: 'INVALID',
+ signature_value: 'SIGNATURE'
+ }
+ end
+
+ it 'is not valid' do
+ expect(subject.valid?).to eq(false)
+ end
+ end
+ end
+
+ describe '#to_hash' do
+ it 'returns a hash representation of the signature' do
+ expect(subject.to_hash).to eq(
+ algorithm_type: params[:algorithm_type],
+ signature_sha: Digest::SHA1.digest(params[:signature_value])
+ )
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/reports/security/locations/sast_spec.rb b/spec/lib/gitlab/ci/reports/security/locations/sast_spec.rb
new file mode 100644
index 00000000000..effa7a60400
--- /dev/null
+++ b/spec/lib/gitlab/ci/reports/security/locations/sast_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Reports::Security::Locations::Sast do
+ let(:params) do
+ {
+ file_path: 'src/main/App.java',
+ start_line: 29,
+ end_line: 31,
+ class_name: 'com.gitlab.security_products.tests.App',
+ method_name: 'insecureCypher'
+ }
+ end
+
+ let(:mandatory_params) { %i[file_path start_line] }
+ let(:expected_fingerprint) { Digest::SHA1.hexdigest('src/main/App.java:29:31') }
+ let(:expected_fingerprint_path) { 'App.java' }
+
+ it_behaves_like 'vulnerability location'
+end
diff --git a/spec/lib/gitlab/ci/reports/security/locations/secret_detection_spec.rb b/spec/lib/gitlab/ci/reports/security/locations/secret_detection_spec.rb
new file mode 100644
index 00000000000..3b84a548713
--- /dev/null
+++ b/spec/lib/gitlab/ci/reports/security/locations/secret_detection_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Reports::Security::Locations::SecretDetection do
+ let(:params) do
+ {
+ file_path: 'src/main/App.java',
+ start_line: 29,
+ end_line: 31,
+ class_name: 'com.gitlab.security_products.tests.App',
+ method_name: 'insecureCypher'
+ }
+ end
+
+ let(:mandatory_params) { %i[file_path start_line] }
+ let(:expected_fingerprint) { Digest::SHA1.hexdigest('src/main/App.java:29:31') }
+ let(:expected_fingerprint_path) { 'App.java' }
+
+ it_behaves_like 'vulnerability location'
+end
diff --git a/spec/lib/gitlab/ci/reports/security/report_spec.rb b/spec/lib/gitlab/ci/reports/security/report_spec.rb
new file mode 100644
index 00000000000..5a85c3f19fc
--- /dev/null
+++ b/spec/lib/gitlab/ci/reports/security/report_spec.rb
@@ -0,0 +1,224 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Reports::Security::Report do
+ let_it_be(:pipeline) { create(:ci_pipeline) }
+
+ let(:created_at) { 2.weeks.ago }
+
+ subject(:report) { described_class.new('sast', pipeline, created_at) }
+
+ it { expect(report.type).to eq('sast') }
+ it { is_expected.to delegate_method(:project_id).to(:pipeline) }
+
+ describe '#add_scanner' do
+ let(:scanner) { create(:ci_reports_security_scanner, external_id: 'find_sec_bugs') }
+
+ subject { report.add_scanner(scanner) }
+
+ it 'stores given scanner params in the map' do
+ subject
+
+ expect(report.scanners).to eq({ 'find_sec_bugs' => scanner })
+ end
+
+ it 'returns the added scanner' do
+ expect(subject).to eq(scanner)
+ end
+ end
+
+ describe '#add_identifier' do
+ let(:identifier) { create(:ci_reports_security_identifier) }
+
+ subject { report.add_identifier(identifier) }
+
+ it 'stores given identifier params in the map' do
+ subject
+
+ expect(report.identifiers).to eq({ identifier.fingerprint => identifier })
+ end
+
+ it 'returns the added identifier' do
+ expect(subject).to eq(identifier)
+ end
+ end
+
+ describe '#add_finding' do
+ let(:finding) { create(:ci_reports_security_finding) }
+
+ it 'enriches given finding and stores it in the collection' do
+ report.add_finding(finding)
+
+ expect(report.findings).to eq([finding])
+ end
+ end
+
+ describe '#clone_as_blank' do
+ let(:report) do
+ create(
+ :ci_reports_security_report,
+ findings: [create(:ci_reports_security_finding)],
+ scanners: [create(:ci_reports_security_scanner)],
+ identifiers: [create(:ci_reports_security_identifier)]
+ )
+ end
+
+ it 'creates a blank report with copied type and pipeline' do
+ clone = report.clone_as_blank
+
+ expect(clone.type).to eq(report.type)
+ expect(clone.pipeline).to eq(report.pipeline)
+ expect(clone.created_at).to eq(report.created_at)
+ expect(clone.findings).to eq([])
+ expect(clone.scanners).to eq({})
+ expect(clone.identifiers).to eq({})
+ end
+ end
+
+ describe '#replace_with!' do
+ let(:report) do
+ create(
+ :ci_reports_security_report,
+ findings: [create(:ci_reports_security_finding)],
+ scanners: [create(:ci_reports_security_scanner)],
+ identifiers: [create(:ci_reports_security_identifier)]
+ )
+ end
+
+ let(:other_report) do
+ create(
+ :ci_reports_security_report,
+ findings: [create(:ci_reports_security_finding, compare_key: 'other_finding')],
+ scanners: [create(:ci_reports_security_scanner, external_id: 'other_scanner', name: 'Other Scanner')],
+ identifiers: [create(:ci_reports_security_identifier, external_id: 'other_id', name: 'other_scanner')]
+ )
+ end
+
+ before do
+ report.replace_with!(other_report)
+ end
+
+ it 'replaces report contents with other reports contents' do
+ expect(report.findings).to eq(other_report.findings)
+ expect(report.scanners).to eq(other_report.scanners)
+ expect(report.identifiers).to eq(other_report.identifiers)
+ end
+ end
+
+ describe '#merge!' do
+ let(:merged_report) { double('Report') }
+
+ before do
+ merge_reports_service = double('MergeReportsService')
+
+ allow(::Security::MergeReportsService).to receive(:new).and_return(merge_reports_service)
+ allow(merge_reports_service).to receive(:execute).and_return(merged_report)
+ allow(report).to receive(:replace_with!)
+ end
+
+ subject { report.merge!(described_class.new('sast', pipeline, created_at)) }
+
+ it 'invokes the merge with other report and then replaces this report contents by merge result' do
+ subject
+
+ expect(report).to have_received(:replace_with!).with(merged_report)
+ end
+ end
+
+ describe '#primary_scanner' do
+ let(:scanner_1) { create(:ci_reports_security_scanner, external_id: 'external_id_1') }
+ let(:scanner_2) { create(:ci_reports_security_scanner, external_id: 'external_id_2') }
+
+ subject { report.primary_scanner }
+
+ before do
+ report.add_scanner(scanner_1)
+ report.add_scanner(scanner_2)
+ end
+
+ it { is_expected.to eq(scanner_1) }
+ end
+
+ describe '#add_error' do
+ context 'when the message is not given' do
+ it 'adds a new error to report with the generic error message' do
+ expect { report.add_error('foo') }.to change { report.errors }
+ .from([])
+ .to([{ type: 'foo', message: 'An unexpected error happened!' }])
+ end
+ end
+
+ context 'when the message is given' do
+ it 'adds a new error to report' do
+ expect { report.add_error('foo', 'bar') }.to change { report.errors }
+ .from([])
+ .to([{ type: 'foo', message: 'bar' }])
+ end
+ end
+ end
+
+ describe 'errored?' do
+ subject { report.errored? }
+
+ context 'when the report does not have any errors' do
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when the report has errors' do
+ before do
+ report.add_error('foo', 'bar')
+ end
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ describe '#primary_scanner_order_to' do
+ let(:scanner_1) { build(:ci_reports_security_scanner) }
+ let(:scanner_2) { build(:ci_reports_security_scanner) }
+ let(:report_1) { described_class.new('sast', pipeline, created_at) }
+ let(:report_2) { described_class.new('sast', pipeline, created_at) }
+
+ subject(:compare_based_on_primary_scanners) { report_1.primary_scanner_order_to(report_2) }
+
+ context 'when the primary scanner of the receiver is nil' do
+ context 'when the primary scanner of the other is nil' do
+ it { is_expected.to be(1) }
+ end
+
+ context 'when the primary scanner of the other is not nil' do
+ before do
+ report_2.add_scanner(scanner_2)
+ end
+
+ it { is_expected.to be(1) }
+ end
+ end
+
+ context 'when the primary scanner of the receiver is not nil' do
+ before do
+ report_1.add_scanner(scanner_1)
+ end
+
+ context 'when the primary scanner of the other is nil' do
+ let(:scanner_2) { nil }
+
+ it { is_expected.to be(-1) }
+ end
+
+ context 'when the primary scanner of the other is not nil' do
+ before do
+ report_2.add_scanner(scanner_2)
+
+ allow(scanner_1).to receive(:<=>).and_return(0)
+ end
+
+ it 'compares two scanners' do
+ expect(compare_based_on_primary_scanners).to be(0)
+ expect(scanner_1).to have_received(:<=>).with(scanner_2)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/reports/security/reports_spec.rb b/spec/lib/gitlab/ci/reports/security/reports_spec.rb
new file mode 100644
index 00000000000..9b1e02f1418
--- /dev/null
+++ b/spec/lib/gitlab/ci/reports/security/reports_spec.rb
@@ -0,0 +1,113 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Reports::Security::Reports do
+ let_it_be(:pipeline) { create(:ci_pipeline) }
+ let_it_be(:artifact) { create(:ci_job_artifact, :sast) }
+
+ let(:security_reports) { described_class.new(pipeline) }
+
+ describe '#get_report' do
+ subject { security_reports.get_report(report_type, artifact) }
+
+ context 'when report type is sast' do
+ let(:report_type) { 'sast' }
+
+ it { expect(subject.type).to eq('sast') }
+ it { expect(subject.created_at).to eq(artifact.created_at) }
+
+ it 'initializes a new report and returns it' do
+ expect(Gitlab::Ci::Reports::Security::Report).to receive(:new)
+ .with('sast', pipeline, artifact.created_at).and_call_original
+
+ is_expected.to be_a(Gitlab::Ci::Reports::Security::Report)
+ end
+
+ context 'when report type is already allocated' do
+ before do
+ subject
+ end
+
+ it 'does not initialize a new report' do
+ expect(Gitlab::Ci::Reports::Security::Report).not_to receive(:new)
+
+ is_expected.to be_a(Gitlab::Ci::Reports::Security::Report)
+ end
+ end
+ end
+ end
+
+ describe '#findings' do
+ let(:finding_1) { build(:ci_reports_security_finding, severity: 'low') }
+ let(:finding_2) { build(:ci_reports_security_finding, severity: 'high') }
+ let!(:expected_findings) { [finding_1, finding_2] }
+
+ subject { security_reports.findings }
+
+ before do
+ security_reports.get_report('sast', artifact).add_finding(finding_1)
+ security_reports.get_report('dependency_scanning', artifact).add_finding(finding_2)
+ end
+
+ it { is_expected.to match_array(expected_findings) }
+ end
+
+ describe "#violates_default_policy_against?" do
+ let(:high_severity_dast) { build(:ci_reports_security_finding, severity: 'high', report_type: :dast) }
+ let(:vulnerabilities_allowed) { 0 }
+ let(:severity_levels) { %w(critical high) }
+
+ subject { security_reports.violates_default_policy_against?(target_reports, vulnerabilities_allowed, severity_levels) }
+
+ before do
+ security_reports.get_report('sast', artifact).add_finding(high_severity_dast)
+ end
+
+ context 'when the target_reports is `nil`' do
+ let(:target_reports) { nil }
+
+ context 'with severity levels matching the existing vulnerabilities' do
+ it { is_expected.to be(true) }
+ end
+
+ context "without any severity levels matching the existing vulnerabilities" do
+ let(:severity_levels) { %w(critical) }
+
+ it { is_expected.to be(false) }
+ end
+ end
+
+ context 'when the target_reports is not `nil`' do
+ let(:target_reports) { described_class.new(pipeline) }
+
+ context "when a report has a new unsafe vulnerability" do
+ context 'with severity levels matching the existing vulnerabilities' do
+ it { is_expected.to be(true) }
+ end
+
+ it { is_expected.to be(true) }
+
+ context 'with vulnerabilities_allowed higher than the number of new vulnerabilities' do
+ let(:vulnerabilities_allowed) { 10000 }
+
+ it { is_expected.to be(false) }
+ end
+
+ context "without any severity levels matching the existing vulnerabilities" do
+ let(:severity_levels) { %w(critical) }
+
+ it { is_expected.to be(false) }
+ end
+ end
+
+ context "when none of the reports have a new unsafe vulnerability" do
+ before do
+ target_reports.get_report('sast', artifact).add_finding(high_severity_dast)
+ end
+
+ it { is_expected.to be(false) }
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/reports/security/vulnerability_reports_comparer_spec.rb b/spec/lib/gitlab/ci/reports/security/vulnerability_reports_comparer_spec.rb
new file mode 100644
index 00000000000..44e66fd9028
--- /dev/null
+++ b/spec/lib/gitlab/ci/reports/security/vulnerability_reports_comparer_spec.rb
@@ -0,0 +1,163 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Reports::Security::VulnerabilityReportsComparer do
+ let(:identifier) { build(:ci_reports_security_identifier) }
+
+ let_it_be(:project) { create(:project, :repository) }
+
+ let(:location_param) { build(:ci_reports_security_locations_sast, :dynamic) }
+ let(:vulnerability_params) { vuln_params(project.id, [identifier], confidence: :low, severity: :critical) }
+ 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(: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]) }
+
+ shared_context 'comparing reports' do
+ let(:vul_params) { vuln_params(project.id, [identifier]) }
+ let(:base_vulnerability) { build(:ci_reports_security_finding, :dynamic, **vul_params) }
+ let(:head_vulnerability) { build(:ci_reports_security_finding, :dynamic, **vul_params) }
+ let(:head_vul_findings) { [head_vulnerability, vuln] }
+ end
+
+ subject { described_class.new(project, base_report, head_report) }
+
+ where(vulnerability_finding_signatures: [true, false])
+
+ with_them do
+ before do
+ stub_licensed_features(vulnerability_finding_signatures: vulnerability_finding_signatures)
+ end
+
+ describe '#base_report_out_of_date' do
+ context 'no base report' do
+ let(:base_report) { build(:ci_reports_security_aggregated_reports, reports: [], findings: []) }
+
+ it 'is not out of date' do
+ expect(subject.base_report_out_of_date).to be false
+ end
+ end
+
+ context 'base report older than one week' do
+ 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]) }
+
+ it 'is not out of date' do
+ expect(subject.base_report_out_of_date).to be true
+ end
+ end
+
+ context 'base report less than one week old' do
+ 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]) }
+
+ it 'is not out of date' do
+ expect(subject.base_report_out_of_date).to be false
+ end
+ end
+ end
+
+ describe '#added' do
+ let(:new_location) {build(:ci_reports_security_locations_sast, :dynamic) }
+ let(:vul_params) { vuln_params(project.id, [identifier], confidence: :high) }
+ 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
+ let(:head_report) { build(:ci_reports_security_aggregated_reports, findings: [head_vulnerability, vuln]) }
+
+ it 'points to source tree' do
+ expect(subject.added).to eq([vuln])
+ end
+ end
+
+ context 'when comparing reports with different fingerprints' do
+ include_context 'comparing reports'
+
+ let(:head_report) { build(:ci_reports_security_aggregated_reports, findings: head_vul_findings) }
+
+ it 'does not find any overlap' do
+ expect(subject.added).to eq(head_vul_findings)
+ end
+ end
+
+ context 'order' do
+ let(:head_report) { build(:ci_reports_security_aggregated_reports, findings: [head_vulnerability, vuln, low_vuln]) }
+
+ it 'does not change' do
+ expect(subject.added).to eq([vuln, low_vuln])
+ end
+ end
+ end
+
+ describe '#fixed' do
+ let(:vul_params) { vuln_params(project.id, [identifier]) }
+ let(:vuln) { build(:ci_reports_security_finding, :dynamic, **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
+ let(:base_report) { build(:ci_reports_security_aggregated_reports, findings: [base_vulnerability, vuln]) }
+
+ it 'points to base tree' do
+ expect(subject.fixed).to eq([vuln])
+ end
+ end
+
+ context 'when comparing reports with different fingerprints' do
+ include_context 'comparing reports'
+
+ let(:base_report) { build(:ci_reports_security_aggregated_reports, findings: [base_vulnerability, vuln]) }
+
+ it 'does not find any overlap' do
+ expect(subject.fixed).to eq([base_vulnerability, vuln])
+ end
+ end
+
+ context 'order' do
+ let(:vul_findings) { [vuln, medium_vuln] }
+ let(:base_report) { build(:ci_reports_security_aggregated_reports, findings: [*vul_findings, base_vulnerability]) }
+
+ it 'does not change' do
+ expect(subject.fixed).to eq(vul_findings)
+ end
+ end
+ end
+
+ describe 'with empty vulnerabilities' do
+ let(:empty_report) { build(:ci_reports_security_aggregated_reports, reports: [], findings: []) }
+
+ it 'returns empty array when reports are not present' do
+ comparer = described_class.new(project, empty_report, empty_report)
+
+ expect(comparer.fixed).to eq([])
+ expect(comparer.added).to eq([])
+ end
+
+ it 'returns added vulnerability when base is empty and head is not empty' do
+ comparer = described_class.new(project, empty_report, head_report)
+
+ expect(comparer.fixed).to eq([])
+ expect(comparer.added).to eq([head_vulnerability])
+ end
+
+ it 'returns fixed vulnerability when head is empty and base is not empty' do
+ comparer = described_class.new(project, base_report, empty_report)
+
+ expect(comparer.fixed).to eq([base_vulnerability])
+ expect(comparer.added).to eq([])
+ 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