diff options
Diffstat (limited to 'lib/gitlab/ci/reports/security/vulnerability_reports_comparer.rb')
-rw-r--r-- | lib/gitlab/ci/reports/security/vulnerability_reports_comparer.rb | 163 |
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 |