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 'lib/gitlab/ci/reports/security/vulnerability_reports_comparer.rb')
-rw-r--r--lib/gitlab/ci/reports/security/vulnerability_reports_comparer.rb163
1 files changed, 163 insertions, 0 deletions
diff --git a/lib/gitlab/ci/reports/security/vulnerability_reports_comparer.rb b/lib/gitlab/ci/reports/security/vulnerability_reports_comparer.rb
new file mode 100644
index 00000000000..6cb2e0ddb33
--- /dev/null
+++ b/lib/gitlab/ci/reports/security/vulnerability_reports_comparer.rb
@@ -0,0 +1,163 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Reports
+ module Security
+ class VulnerabilityReportsComparer
+ include Gitlab::Utils::StrongMemoize
+
+ attr_reader :base_report, :head_report
+
+ ACCEPTABLE_REPORT_AGE = 1.week
+
+ def initialize(project, base_report, head_report)
+ @base_report = base_report
+ @head_report = head_report
+
+ @signatures_enabled = project.licensed_feature_available?(:vulnerability_finding_signatures)
+
+ if @signatures_enabled
+ @added_findings = []
+ @fixed_findings = []
+ calculate_changes
+ end
+ end
+
+ def base_report_created_at
+ @base_report.created_at
+ end
+
+ def head_report_created_at
+ @head_report.created_at
+ end
+
+ def base_report_out_of_date
+ return false unless @base_report.created_at
+
+ ACCEPTABLE_REPORT_AGE.ago > @base_report.created_at
+ end
+
+ def added
+ strong_memoize(:added) do
+ if @signatures_enabled
+ @added_findings
+ else
+ head_report.findings - base_report.findings
+ end
+ end
+ end
+
+ def fixed
+ strong_memoize(:fixed) do
+ if @signatures_enabled
+ @fixed_findings
+ else
+ base_report.findings - head_report.findings
+ end
+ end
+ end
+
+ private
+
+ def calculate_changes
+ # This is a deconstructed version of the eql? method on
+ # Ci::Reports::Security::Finding. It:
+ #
+ # * precomputes for the head_findings (using FindingMatcher):
+ # * sets of signature shas grouped by priority
+ # * mappings of signature shas to the head finding object
+ #
+ # These are then used when iterating the base findings to perform
+ # fast(er) prioritized, signature-based comparisons between each base finding
+ # and the head findings.
+ #
+ # Both the head_findings and base_findings arrays are iterated once
+
+ base_findings = base_report.findings
+ head_findings = head_report.findings
+
+ matcher = FindingMatcher.new(head_findings)
+
+ base_findings.each do |base_finding|
+ matched_head_finding = matcher.find_and_remove_match!(base_finding)
+
+ @fixed_findings << base_finding if matched_head_finding.nil?
+ end
+
+ @added_findings = matcher.unmatched_head_findings.values
+ end
+ end
+
+ class FindingMatcher
+ attr_reader :unmatched_head_findings, :head_findings
+
+ include Gitlab::Utils::StrongMemoize
+
+ def initialize(head_findings)
+ @head_findings = head_findings
+ @unmatched_head_findings = @head_findings.index_by(&:object_id)
+ end
+
+ def find_and_remove_match!(base_finding)
+ matched_head_finding = find_matched_head_finding_for(base_finding)
+
+ # no signatures matched, so check the normal uuids of the base and head findings
+ # for a match
+ matched_head_finding = head_signatures_shas[base_finding.uuid] if matched_head_finding.nil?
+
+ @unmatched_head_findings.delete(matched_head_finding.object_id) unless matched_head_finding.nil?
+
+ matched_head_finding
+ end
+
+ private
+
+ def find_matched_head_finding_for(base_finding)
+ base_signature = sorted_signatures_for(base_finding).find do |signature|
+ # at this point a head_finding exists that has a signature with a
+ # matching priority, and a matching sha --> lookup the actual finding
+ # object from head_signatures_shas
+ head_signatures_shas[signature.signature_sha].eql?(base_finding)
+ end
+
+ base_signature.present? ? head_signatures_shas[base_signature.signature_sha] : nil
+ end
+
+ def sorted_signatures_for(base_finding)
+ base_finding.signatures.select { |signature| head_finding_signature?(signature) }
+ .sort_by { |sig| -sig.priority }
+ end
+
+ def head_finding_signature?(signature)
+ head_signatures_priorities[signature.priority].include?(signature.signature_sha)
+ end
+
+ def head_signatures_priorities
+ strong_memoize(:head_signatures_priorities) do
+ signatures_priorities = Hash.new { |hash, key| hash[key] = Set.new }
+
+ head_findings.each_with_object(signatures_priorities) do |head_finding, memo|
+ head_finding.signatures.each do |signature|
+ memo[signature.priority].add(signature.signature_sha)
+ end
+ end
+ end
+ end
+
+ def head_signatures_shas
+ strong_memoize(:head_signatures_shas) do
+ head_findings.each_with_object({}) do |head_finding, memo|
+ head_finding.signatures.each do |signature|
+ memo[signature.signature_sha] = head_finding
+ end
+ # for the final uuid check when no signatures have matched
+ memo[head_finding.uuid] = head_finding
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end