summaryrefslogtreecommitdiff
path: root/lib/gitlab/ci/reports/security/vulnerability_reports_comparer.rb
blob: 4be4cf62e7b1858d362af98f94d352856416eb0c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# 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|
              next if base_finding.requires_manual_resolution?

              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